mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 12:31:55 -05:00
Compare commits
33 Commits
todo-examp
...
979
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f8b6dc440 | ||
|
|
af561abdf8 | ||
|
|
fcb98474b8 | ||
|
|
54f7e9366a | ||
|
|
ddf9df2b5e | ||
|
|
7fe9f82d89 | ||
|
|
661adc4027 | ||
|
|
1011c464dc | ||
|
|
4b498a3b42 | ||
|
|
3c90b47e77 | ||
|
|
671b1e4a8f | ||
|
|
52021be806 | ||
|
|
75a7bd610a | ||
|
|
de553cf4fe | ||
|
|
0a65f43789 | ||
|
|
0f277c55ec | ||
|
|
04b01a6ced | ||
|
|
6c3381ce52 | ||
|
|
fa2e2248d3 | ||
|
|
362150a715 | ||
|
|
27b5991ee3 | ||
|
|
0a7dbb0ca4 | ||
|
|
234861a156 | ||
|
|
78d6d312f8 | ||
|
|
a1144a5b6b | ||
|
|
9723cc466e | ||
|
|
79c12c0129 | ||
|
|
a08d6bae10 | ||
|
|
39261a276c | ||
|
|
c471986024 | ||
|
|
d2e3a156e8 | ||
|
|
9badfa997b | ||
|
|
72f8bf4e20 |
28
Cargo.toml
28
Cargo.toml
@@ -25,22 +25,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-alpha"
|
||||
|
||||
[workspace.dependencies]
|
||||
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" }
|
||||
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" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -69,13 +69,27 @@ dependencies = [
|
||||
|
||||
[tasks.test]
|
||||
clear = true
|
||||
dependencies = ["test-all"]
|
||||
dependencies = ["test-all", "test-leptos_macro-example", "doc-leptos_macro-example"]
|
||||
|
||||
[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"
|
||||
@@ -90,6 +104,7 @@ args = ["make", "verify-flow"]
|
||||
|
||||
[env]
|
||||
RUSTFLAGS = ""
|
||||
LEPTOS_OUTPUT_NAME="ci" # allows examples to check/build without cargo-leptos
|
||||
|
||||
[env.github-actions]
|
||||
RUSTFLAGS = "-D warnings"
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
- [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)
|
||||
|
||||
177
docs/book/src/interlude_projecting_children.md
Normal file
177
docs/book/src/interlude_projecting_children.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# 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 we’re 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 won’t 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. Here’s 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 can’t 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 don’t 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, it’s 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
|
||||
```
|
||||
|
||||
It’s captured through multiple levels of children that need to run more than once, and there’s 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.
|
||||
@@ -65,7 +65,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
};
|
||||
data.into_iter()
|
||||
.map(|value| view! { cx, <span>{value}</span> })
|
||||
.collect::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -52,6 +52,12 @@ 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 you’re 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
|
||||
|
||||
@@ -31,6 +31,22 @@ 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 doesn’t mean the interface needs to be static.
|
||||
You can render dynamic items as part of a static list.
|
||||
|
||||
@@ -52,7 +68,7 @@ let counter_buttons = counters
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect_view(cx);
|
||||
|
||||
view! { cx,
|
||||
<ul>{counter_buttons}</ul>
|
||||
|
||||
@@ -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::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -103,7 +103,7 @@ pub fn WrapsChildren(cx: Scope, children: Children) -> impl IntoView {
|
||||
.nodes
|
||||
.into_iter()
|
||||
.map(|child| view! { cx, <li>{child}</li> })
|
||||
.collect::<Vec<_>>();
|
||||
.collect_view(cx);
|
||||
|
||||
view! { cx,
|
||||
<ul>{children}</ul>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
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",
|
||||
@@ -42,10 +41,6 @@ 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]
|
||||
|
||||
14
examples/cargo-make/common.toml
Normal file
14
examples/cargo-make/common.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[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"]
|
||||
4
examples/cargo-make/wasm-web-test.toml
Normal file
4
examples/cargo-make/wasm-web-test.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
[tasks.web-test]
|
||||
env = { CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome" }
|
||||
command = "cargo"
|
||||
args = ["make", "wasm-pack-test"]
|
||||
@@ -1,3 +1,8 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/common.toml" },
|
||||
{ path = "../cargo-make/wasm-web-test.toml" },
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ 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
|
||||
/>
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
document.body().unwrap().append_child(&test_wrapper);
|
||||
let _ = 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();
|
||||
document.body().unwrap().append_child(&test_wrapper);
|
||||
let _ = 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()
|
||||
@@ -17,6 +17,7 @@ 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"]
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
[env]
|
||||
CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome"
|
||||
|
||||
[tasks.web-test]
|
||||
command = "cargo"
|
||||
args = ["make", "wasm-pack-test"]
|
||||
extend = [
|
||||
{ path = "../cargo-make/common.toml" },
|
||||
{ path = "../cargo-make/wasm-web-test.toml" },
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
|
||||
@@ -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: i32) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, initial_value);
|
||||
pub fn counter(cx: Scope, initial_value: i32, step: u32) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, Count::new(initial_value, step));
|
||||
|
||||
// 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: i32) -> 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_value.update(|value| *value = 0))
|
||||
.on(ev::click, move |_| set_count.update(|count| count.clear()))
|
||||
.child("Clear"),
|
||||
)
|
||||
.child(
|
||||
button(cx)
|
||||
.on(ev::click, move |_| {
|
||||
set_value.update(|value| *value -= step)
|
||||
set_count.update(|count| count.decrease())
|
||||
})
|
||||
.child("-1"),
|
||||
)
|
||||
@@ -31,14 +31,45 @@ pub fn counter(cx: Scope, initial_value: i32, step: i32) -> impl IntoView {
|
||||
.child("Value: ")
|
||||
// reactive values are passed to .child() as a tuple
|
||||
// (Scope, [child function]) so an effect can be created
|
||||
.child((cx, move || value.get()))
|
||||
.child(move || count.get().value())
|
||||
.child("!"),
|
||||
)
|
||||
.child(
|
||||
button(cx)
|
||||
.on(ev::click, move |_| {
|
||||
set_value.update(|value| *value += step)
|
||||
set_count.update(|count| count.increase())
|
||||
})
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
49
examples/counter_without_macros/tests/business.rs
Normal file
49
examples/counter_without_macros/tests/business.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/common.toml" },
|
||||
{ path = "../cargo-make/wasm-web-test.toml" },
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
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>");
|
||||
}
|
||||
120
examples/counters/tests/web.rs
Normal file
120
examples/counters/tests/web.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
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>"
|
||||
);
|
||||
}
|
||||
@@ -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::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,7 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
|
||||
errors
|
||||
.iter()
|
||||
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li> })
|
||||
.collect::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
})
|
||||
};
|
||||
|
||||
@@ -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::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
@@ -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::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
@@ -241,11 +241,11 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
todos.read(cx)
|
||||
.map(move |todos| match todos {
|
||||
Err(e) => {
|
||||
vec![view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_any()]
|
||||
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
|
||||
}
|
||||
Ok(todos) => {
|
||||
if todos.is_empty() {
|
||||
vec![view! { cx, <p>"No tasks were found."</p> }.into_any()]
|
||||
view! { cx, <p>"No tasks were found."</p> }.into_view(cx)
|
||||
} else {
|
||||
todos
|
||||
.into_iter()
|
||||
@@ -266,9 +266,8 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
</ActionForm>
|
||||
</li>
|
||||
}
|
||||
.into_any()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -287,7 +286,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
};
|
||||
|
||||
view! {
|
||||
|
||||
@@ -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::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
})
|
||||
)
|
||||
};
|
||||
@@ -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::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -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::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
})
|
||||
)
|
||||
};
|
||||
@@ -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::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main(){
|
||||
async fn main() {
|
||||
use axum::{
|
||||
extract::{Extension, Path},
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use leptos::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use axum::{extract::{Extension, Path}, Router, routing::{get, post}};
|
||||
use ssr_modes_axum::{app::*, fallback::file_and_error_handler};
|
||||
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;
|
||||
@@ -19,7 +22,11 @@ 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)));
|
||||
|
||||
|
||||
24
examples/timer/Cargo.toml
Normal file
24
examples/timer/Cargo.toml
Normal file
@@ -0,0 +1,24 @@
|
||||
[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"
|
||||
9
examples/timer/Makefile.toml
Normal file
9
examples/timer/Makefile.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[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"
|
||||
7
examples/timer/README.md
Normal file
7
examples/timer/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# 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/)
|
||||
8
examples/timer/index.html
Normal file
8
examples/timer/index.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<!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>
|
||||
BIN
examples/timer/public/favicon.ico
Normal file
BIN
examples/timer/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
2
examples/timer/rust-toolchain.toml
Normal file
2
examples/timer/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
61
examples/timer/src/lib.rs
Normal file
61
examples/timer/src/lib.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
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")
|
||||
});
|
||||
}
|
||||
12
examples/timer/src/main.rs
Normal file
12
examples/timer/src/main.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
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 />
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -176,8 +176,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.into_view(cx)
|
||||
.collect_view(cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -196,7 +195,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
};
|
||||
|
||||
view! {
|
||||
|
||||
@@ -13,7 +13,9 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,8 @@ 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
|
||||
@@ -109,19 +110,25 @@ 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(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +153,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
|
||||
@@ -222,8 +229,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.into_view(cx)
|
||||
.collect_view(cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -242,7 +248,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
};
|
||||
|
||||
view! {
|
||||
|
||||
@@ -183,8 +183,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.into_view(cx)
|
||||
.collect_view(cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -203,7 +202,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
};
|
||||
|
||||
view! {
|
||||
|
||||
@@ -202,6 +202,19 @@ 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">
|
||||
|
||||
@@ -13,7 +13,7 @@ use actix_web::{
|
||||
web::Bytes,
|
||||
*,
|
||||
};
|
||||
use futures::{Future, Stream, StreamExt};
|
||||
use futures::{Stream, StreamExt};
|
||||
use http::StatusCode;
|
||||
use leptos::{
|
||||
leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context,
|
||||
@@ -26,7 +26,7 @@ use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use parking_lot::RwLock;
|
||||
use regex::Regex;
|
||||
use std::sync::Arc;
|
||||
use std::{fmt::Display, future::Future, 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.
|
||||
@@ -905,6 +905,20 @@ async fn render_app_async_helper(
|
||||
pub fn generate_route_list<IV>(
|
||||
app_fn: impl FnOnce(leptos::Scope) -> IV + 'static,
|
||||
) -> Vec<RouteListing>
|
||||
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,
|
||||
{
|
||||
@@ -932,7 +946,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 routes = routes
|
||||
let mut routes = routes
|
||||
.into_iter()
|
||||
.map(|listing| {
|
||||
let path = wildcard_re
|
||||
@@ -946,6 +960,10 @@ 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
|
||||
}
|
||||
}
|
||||
@@ -1094,3 +1112,94 @@ where
|
||||
router
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 }
|
||||
|
||||
@@ -19,7 +19,10 @@ use futures::{
|
||||
channel::mpsc::{Receiver, Sender},
|
||||
Future, SinkExt, Stream, StreamExt,
|
||||
};
|
||||
use http::{header, method::Method, uri::Uri, version::Version, Response};
|
||||
use http::{
|
||||
header, method::Method, request::Parts, uri::Uri, version::Version,
|
||||
Response,
|
||||
};
|
||||
use hyper::body;
|
||||
use leptos::{
|
||||
leptos_server::{server_fn_by_path, Payload},
|
||||
@@ -46,6 +49,21 @@ 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)]
|
||||
@@ -1003,6 +1021,21 @@ 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,
|
||||
{
|
||||
@@ -1029,7 +1062,7 @@ where
|
||||
|
||||
let routes = routes.0.read().to_owned();
|
||||
// Axum's Router defines Root routes as "/" not ""
|
||||
let routes = routes
|
||||
let mut routes = routes
|
||||
.into_iter()
|
||||
.map(|listing| {
|
||||
let path = listing.path();
|
||||
@@ -1052,6 +1085,10 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
|
||||
// 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.
|
||||
let mut wasm_output_name = output_name.clone();
|
||||
if std::env::var("LEPTOS_OUTPUT_NAME").is_err() {
|
||||
if std::option_env!("LEPTOS_OUTPUT_NAME").is_none() {
|
||||
wasm_output_name.push_str("_bg");
|
||||
}
|
||||
|
||||
|
||||
@@ -947,6 +947,19 @@ 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,
|
||||
{
|
||||
@@ -973,7 +986,7 @@ where
|
||||
|
||||
let routes = routes.0.read().to_owned();
|
||||
// Viz's Router defines Root routes as "/" not ""
|
||||
let routes = routes
|
||||
let mut routes = routes
|
||||
.into_iter()
|
||||
.map(|listing| {
|
||||
let path = listing.path();
|
||||
@@ -996,6 +1009,9 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,18 +45,20 @@ where
|
||||
provide_context(cx, errors);
|
||||
|
||||
// Run children so that they render and execute resources
|
||||
let children = children(cx);
|
||||
let children = children(cx).into_view(cx);
|
||||
let errors_empty = create_memo(cx, move |_| errors.with(Errors::is_empty));
|
||||
|
||||
move || {
|
||||
match errors.with(Errors::is_empty) {
|
||||
true => children.clone().into_view(cx),
|
||||
false => view! { cx,
|
||||
if errors_empty.get() {
|
||||
children.clone().into_view(cx)
|
||||
} else {
|
||||
view! { cx,
|
||||
<>
|
||||
{fallback(cx, errors)}
|
||||
<leptos-error-boundary style="display: none">{children.clone()}</leptos-error-boundary>
|
||||
</>
|
||||
}
|
||||
.into_view(cx),
|
||||
.into_view(cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, Errors, Fragment, HtmlElement, IntoAttribute, IntoClass,
|
||||
IntoProperty, IntoView, NodeRef, Property, View,
|
||||
Class, CollectView, Errors, Fragment, HtmlElement, IntoAttribute,
|
||||
IntoClass, IntoProperty, IntoView, NodeRef, Property, View,
|
||||
};
|
||||
pub use leptos_macro::*;
|
||||
pub use leptos_reactive::*;
|
||||
|
||||
@@ -29,18 +29,15 @@ 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_any(),
|
||||
/// Some(cats) => view! { cx,
|
||||
/// <div>{
|
||||
/// cats.iter()
|
||||
/// .map(|src| {
|
||||
/// None => view! { cx, <pre>"Error"</pre> }.into_view(cx),
|
||||
/// Some(cats) => cats
|
||||
/// .iter()
|
||||
/// .map(|src| {
|
||||
/// view! { cx,
|
||||
/// <img src={src}/>
|
||||
/// }
|
||||
/// })
|
||||
/// .collect::<Vec<_>>()
|
||||
/// }</div>
|
||||
/// }.into_any(),
|
||||
/// })
|
||||
/// .collect_view(cx),
|
||||
/// })
|
||||
/// }
|
||||
/// }
|
||||
|
||||
@@ -39,18 +39,15 @@ use std::{
|
||||
/// >
|
||||
/// {move || {
|
||||
/// cats.read(cx).map(|data| match data {
|
||||
/// None => view! { cx, <pre>"Error"</pre> }.into_any(),
|
||||
/// Some(cats) => view! { cx,
|
||||
/// <div>{
|
||||
/// cats.iter()
|
||||
/// .map(|src| {
|
||||
/// None => view! { cx, <pre>"Error"</pre> }.into_view(cx),
|
||||
/// Some(cats) => cats
|
||||
/// .iter()
|
||||
/// .map(|src| {
|
||||
/// view! { cx,
|
||||
/// <img src={src}/>
|
||||
/// }
|
||||
/// })
|
||||
/// .collect::<Vec<_>>()
|
||||
/// }</div>
|
||||
/// }.into_any(),
|
||||
/// })
|
||||
/// .collect_view(cx),
|
||||
/// })
|
||||
/// }
|
||||
/// }
|
||||
|
||||
@@ -53,12 +53,23 @@ 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: std::env::var("LEPTOS_OUTPUT_NAME").map_err(|e| {
|
||||
LeptosConfigError::EnvVarError(format!(
|
||||
"LEPTOS_OUTPUT_NAME: {e}"
|
||||
))
|
||||
})?,
|
||||
output_name,
|
||||
site_root: env_w_default("LEPTOS_SITE_ROOT", "target/site")?,
|
||||
site_pkg_dir: env_w_default("LEPTOS_SITE_PKG_DIR", "pkg")?,
|
||||
env: Env::default(),
|
||||
|
||||
@@ -31,9 +31,6 @@ 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");
|
||||
@@ -51,19 +48,4 @@ 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);
|
||||
}
|
||||
|
||||
@@ -140,9 +140,6 @@ 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");
|
||||
|
||||
@@ -140,18 +140,23 @@ 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 {
|
||||
#[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)
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,21 @@ 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)]
|
||||
@@ -91,16 +106,7 @@ 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, 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)
|
||||
fn into_view(self, _: leptos_reactive::Scope) -> View {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,6 +209,25 @@ 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.
|
||||
@@ -533,6 +552,12 @@ 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 {
|
||||
|
||||
11
leptos_macro/example/Cargo.toml
Normal file
11
leptos_macro/example/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[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]
|
||||
41
leptos_macro/example/src/lib.rs
Normal file
41
leptos_macro/example/src/lib.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
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!()
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ use convert_case::{
|
||||
Casing,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
use quote::{format_ident, ToTokens, TokenStreamExt};
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{format_ident, quote_spanned, ToTokens, TokenStreamExt};
|
||||
use syn::{
|
||||
parse::Parse, parse_quote, AngleBracketedGenericArguments, Attribute,
|
||||
FnArg, GenericArgument, ItemFn, LitStr, Meta, MetaNameValue, Pat, PatIdent,
|
||||
Path, PathArguments, ReturnType, Type, TypePath, Visibility,
|
||||
FnArg, GenericArgument, ItemFn, Lit, LitStr, Meta, MetaNameValue, Pat,
|
||||
PatIdent, Path, PathArguments, ReturnType, Type, TypePath, Visibility,
|
||||
};
|
||||
|
||||
pub struct Model {
|
||||
@@ -208,6 +208,12 @@ 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)]
|
||||
@@ -285,14 +291,14 @@ impl Prop {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Docs(Vec<Attribute>);
|
||||
pub struct Docs(Vec<(String, Span)>);
|
||||
|
||||
impl ToTokens for Docs {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let s = self
|
||||
.0
|
||||
.iter()
|
||||
.map(|attr| attr.to_token_stream())
|
||||
.map(|(doc, span)| quote_spanned!(*span=> #[doc = #doc]))
|
||||
.collect::<TokenStream>();
|
||||
|
||||
tokens.append_all(s);
|
||||
@@ -301,11 +307,96 @@ impl ToTokens for Docs {
|
||||
|
||||
impl Docs {
|
||||
pub fn new(attrs: &[Attribute]) -> Self {
|
||||
let attrs = attrs
|
||||
#[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
|
||||
.iter()
|
||||
.filter(|attr| attr.path == parse_quote!(doc))
|
||||
.cloned()
|
||||
.collect();
|
||||
.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()))
|
||||
}
|
||||
|
||||
Self(attrs)
|
||||
}
|
||||
@@ -314,57 +405,22 @@ impl Docs {
|
||||
self.0
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, attr)| {
|
||||
match attr.parse_meta() {
|
||||
Ok(Meta::NameValue(MetaNameValue { lit: doc, .. })) => {
|
||||
let doc_str = quote!(#doc);
|
||||
.map(|(idx, (doc, span))| {
|
||||
let doc = if idx == 0 {
|
||||
format!(" - {doc}")
|
||||
} else {
|
||||
format!(" {doc}")
|
||||
};
|
||||
|
||||
// We need to remove the leading and trailing `"`"
|
||||
let mut doc_str = doc_str.to_string();
|
||||
doc_str.pop();
|
||||
doc_str.remove(0);
|
||||
let doc = LitStr::new(&doc, *span);
|
||||
|
||||
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"),
|
||||
}
|
||||
quote! { #[doc = #doc] }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn typed_builder(&self) -> String {
|
||||
#[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>();
|
||||
let doc_str = self.0.iter().map(|s| s.0.as_str()).join("\n");
|
||||
|
||||
if doc_str.chars().filter(|c| *c != '\n').count() != 0 {
|
||||
format!("\n\n{doc_str}")
|
||||
@@ -458,10 +514,18 @@ 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,
|
||||
}
|
||||
})
|
||||
@@ -606,12 +670,11 @@ fn prop_to_doc(
|
||||
PropDocStyle::List => {
|
||||
let arg_ty_doc = LitStr::new(
|
||||
&if !prop_opts.into {
|
||||
format!("- **{}**: [`{}`]", quote!(#name), pretty_ty)
|
||||
format!("- **{}**: [`{pretty_ty}`]", quote!(#name))
|
||||
} else {
|
||||
format!(
|
||||
"- **{}**: `impl`[`Into<{}>`]",
|
||||
"- **{}**: [`impl Into<{pretty_ty}>`]({pretty_ty})",
|
||||
quote!(#name),
|
||||
pretty_ty
|
||||
)
|
||||
},
|
||||
name.ident.span(),
|
||||
|
||||
@@ -628,9 +628,7 @@ 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);
|
||||
|
||||
let transparent_token: syn::Ident = syn::parse_quote!(transparent);
|
||||
|
||||
if transparent != transparent_token {
|
||||
if transparent != "transparent" {
|
||||
abort!(
|
||||
transparent,
|
||||
"only `transparent` is supported";
|
||||
@@ -818,8 +816,9 @@ 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_urlencoded`](https://docs.rs/serde_urlencoded/latest/serde_urlencoded/) or as `application/cbor`
|
||||
/// using [`cbor`](https://docs.rs/cbor/latest/cbor/).
|
||||
/// form data using [`serde_html_form`](https://docs.rs/serde_html_form/latest/serde_html_form/) 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`.
|
||||
/// - **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.
|
||||
|
||||
@@ -81,8 +81,8 @@ impl ToTokens for Model {
|
||||
#prop_builder_fields
|
||||
}
|
||||
|
||||
impl From<#name> for Vec<#name> {
|
||||
fn from(value: #name) -> Self {
|
||||
impl #generics From<#name #generics> for Vec<#name #generics> #where_clause {
|
||||
fn from(value: #name #generics) -> Self {
|
||||
vec![value]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,23 @@ use leptos::*;
|
||||
fn missing_scope() {}
|
||||
|
||||
#[component]
|
||||
fn missing_return_type(cx: Scope) {}
|
||||
fn missing_return_type(cx: Scope) {
|
||||
_ = cx;
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn unknown_prop_option(cx: Scope, #[prop(hello)] test: bool) -> impl IntoView {}
|
||||
fn unknown_prop_option(cx: Scope, #[prop(hello)] test: bool) -> impl IntoView {
|
||||
_ = cx;
|
||||
_ = test;
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn optional_and_optional_no_strip(
|
||||
cx: Scope,
|
||||
#[prop(optional, optional_no_strip)] conflicting: bool,
|
||||
) -> impl IntoView {
|
||||
_ = cx;
|
||||
_ = conflicting;
|
||||
}
|
||||
|
||||
#[component]
|
||||
@@ -21,6 +28,8 @@ fn optional_and_strip_option(
|
||||
cx: Scope,
|
||||
#[prop(optional, strip_option)] conflicting: bool,
|
||||
) -> impl IntoView {
|
||||
_ = cx;
|
||||
_ = conflicting;
|
||||
}
|
||||
|
||||
#[component]
|
||||
@@ -28,6 +37,8 @@ fn optional_no_strip_and_strip_option(
|
||||
cx: Scope,
|
||||
#[prop(optional_no_strip, strip_option)] conflicting: bool,
|
||||
) -> impl IntoView {
|
||||
_ = cx;
|
||||
_ = conflicting;
|
||||
}
|
||||
|
||||
#[component]
|
||||
@@ -35,6 +46,8 @@ fn default_without_value(
|
||||
cx: Scope,
|
||||
#[prop(default)] default: bool,
|
||||
) -> impl IntoView {
|
||||
_ = cx;
|
||||
_ = default;
|
||||
}
|
||||
|
||||
#[component]
|
||||
@@ -42,6 +55,8 @@ fn default_with_invalid_value(
|
||||
cx: Scope,
|
||||
#[prop(default= |)] default: bool,
|
||||
) -> impl IntoView {
|
||||
_ = cx;
|
||||
_ = default;
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
|
||||
@@ -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:10:42
|
||||
--> tests/ui/component.rs:12:42
|
||||
|
|
||||
10 | fn unknown_prop_option(cx: Scope, #[prop(hello)] test: bool) -> impl IntoView {}
|
||||
12 | 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:15:12
|
||||
--> tests/ui/component.rs:20:12
|
||||
|
|
||||
15 | #[prop(optional, optional_no_strip)] conflicting: bool,
|
||||
20 | #[prop(optional, optional_no_strip)] conflicting: bool,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: `optional` conflicts with mutually exclusive `strip_option`
|
||||
--> tests/ui/component.rs:22:12
|
||||
--> tests/ui/component.rs:29:12
|
||||
|
|
||||
22 | #[prop(optional, strip_option)] conflicting: bool,
|
||||
29 | #[prop(optional, strip_option)] conflicting: bool,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: `optional_no_strip` conflicts with mutually exclusive `strip_option`
|
||||
--> tests/ui/component.rs:29:12
|
||||
--> tests/ui/component.rs:38:12
|
||||
|
|
||||
29 | #[prop(optional_no_strip, strip_option)] conflicting: bool,
|
||||
38 | #[prop(optional_no_strip, strip_option)] conflicting: bool,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: unexpected end of input, expected assignment `=`
|
||||
--> tests/ui/component.rs:36:19
|
||||
--> tests/ui/component.rs:47:19
|
||||
|
|
||||
36 | #[prop(default)] default: bool,
|
||||
47 | #[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:43:22
|
||||
--> tests/ui/component.rs:56:22
|
||||
|
|
||||
43 | #[prop(default= |)] default: bool,
|
||||
56 | #[prop(default= |)] default: bool,
|
||||
| ^
|
||||
|
||||
@@ -2,16 +2,23 @@
|
||||
fn missing_scope() {}
|
||||
|
||||
#[::leptos::component]
|
||||
fn missing_return_type(cx: ::leptos::Scope) {}
|
||||
fn missing_return_type(cx: ::leptos::Scope) {
|
||||
_ = cx;
|
||||
}
|
||||
|
||||
#[::leptos::component]
|
||||
fn unknown_prop_option(cx: ::leptos::Scope, #[prop(hello)] test: bool) -> impl ::leptos::IntoView {}
|
||||
fn unknown_prop_option(cx: ::leptos::Scope, #[prop(hello)] test: bool) -> impl ::leptos::IntoView {
|
||||
_ = cx;
|
||||
_ = test;
|
||||
}
|
||||
|
||||
#[::leptos::component]
|
||||
fn optional_and_optional_no_strip(
|
||||
cx: Scope,
|
||||
#[prop(optional, optional_no_strip)] conflicting: bool,
|
||||
) -> impl IntoView {
|
||||
_ = cx;
|
||||
_ = conflicting;
|
||||
}
|
||||
|
||||
#[::leptos::component]
|
||||
@@ -19,6 +26,8 @@ fn optional_and_strip_option(
|
||||
cx: ::leptos::Scope,
|
||||
#[prop(optional, strip_option)] conflicting: bool,
|
||||
) -> impl ::leptos::IntoView {
|
||||
_ = cx;
|
||||
_ = conflicting;
|
||||
}
|
||||
|
||||
#[::leptos::component]
|
||||
@@ -26,6 +35,8 @@ 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]
|
||||
@@ -33,6 +44,8 @@ fn default_without_value(
|
||||
cx: ::leptos::Scope,
|
||||
#[prop(default)] default: bool,
|
||||
) -> impl ::leptos::IntoView {
|
||||
_ = cx;
|
||||
_ = default;
|
||||
}
|
||||
|
||||
#[::leptos::component]
|
||||
@@ -40,10 +53,13 @@ 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"
|
||||
}
|
||||
|
||||
@@ -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:8:52
|
||||
|
|
||||
8 | fn unknown_prop_option(cx: ::leptos::Scope, #[prop(hello)] test: bool) -> impl ::leptos::IntoView {}
|
||||
| ^^^^^
|
||||
--> tests/ui/component_absolute.rs:10:52
|
||||
|
|
||||
10 | 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:13:12
|
||||
--> tests/ui/component_absolute.rs:18:12
|
||||
|
|
||||
13 | #[prop(optional, optional_no_strip)] conflicting: bool,
|
||||
18 | #[prop(optional, optional_no_strip)] conflicting: bool,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: `optional` conflicts with mutually exclusive `strip_option`
|
||||
--> tests/ui/component_absolute.rs:20:12
|
||||
--> tests/ui/component_absolute.rs:27:12
|
||||
|
|
||||
20 | #[prop(optional, strip_option)] conflicting: bool,
|
||||
27 | #[prop(optional, strip_option)] conflicting: bool,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: `optional_no_strip` conflicts with mutually exclusive `strip_option`
|
||||
--> tests/ui/component_absolute.rs:27:12
|
||||
--> tests/ui/component_absolute.rs:36:12
|
||||
|
|
||||
27 | #[prop(optional_no_strip, strip_option)] conflicting: bool,
|
||||
36 | #[prop(optional_no_strip, strip_option)] conflicting: bool,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: unexpected end of input, expected assignment `=`
|
||||
--> tests/ui/component_absolute.rs:34:19
|
||||
--> tests/ui/component_absolute.rs:45:19
|
||||
|
|
||||
34 | #[prop(default)] default: bool,
|
||||
45 | #[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:41:22
|
||||
--> tests/ui/component_absolute.rs:54:22
|
||||
|
|
||||
41 | #[prop(default= |)] default: bool,
|
||||
54 | #[prop(default= |)] default: bool,
|
||||
| ^
|
||||
|
||||
@@ -842,17 +842,23 @@ where
|
||||
_ = location;
|
||||
}
|
||||
#[cfg(all(feature = "hydrate", debug_assertions))]
|
||||
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.)",
|
||||
);
|
||||
{
|
||||
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.)",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let increment = move |_: Option<()>| {
|
||||
|
||||
@@ -72,8 +72,9 @@
|
||||
//! This should be fairly obvious: we have to serialize arguments to send them to the server, and we
|
||||
//! need to deserialize the result to return it to the client.
|
||||
//! - **Arguments must be implement [serde::Serialize].** They are serialized as an `application/x-www-form-urlencoded`
|
||||
//! form data using [`serde_urlencoded`](https://docs.rs/serde_urlencoded/latest/serde_urlencoded/) or as `application/cbor`
|
||||
//! using [`cbor`](https://docs.rs/cbor/latest/cbor/).
|
||||
//! form data using [`serde_html_form`](https://docs.rs/serde_html_form/latest/serde_html_form/) 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`.
|
||||
//! - **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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-alpha"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-alpha"
|
||||
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_urlencoded = "0.7"
|
||||
serde_html_form = "0.2"
|
||||
serde = "1"
|
||||
tracing = "0.1"
|
||||
js-sys = { version = "0.3" }
|
||||
|
||||
@@ -539,12 +539,12 @@ where
|
||||
/// Tries to deserialize the data, given only the `submit` event.
|
||||
fn from_event(
|
||||
ev: &web_sys::Event,
|
||||
) -> Result<Self, serde_urlencoded::de::Error>;
|
||||
) -> Result<Self, serde_html_form::de::Error>;
|
||||
|
||||
/// Tries to deserialize the data, given the actual form data.
|
||||
fn from_form_data(
|
||||
form_data: &web_sys::FormData,
|
||||
) -> Result<Self, serde_urlencoded::de::Error>;
|
||||
) -> Result<Self, serde_html_form::de::Error>;
|
||||
}
|
||||
|
||||
impl<T> FromFormData for T
|
||||
@@ -557,7 +557,7 @@ where
|
||||
)]
|
||||
fn from_event(
|
||||
ev: &web_sys::Event,
|
||||
) -> Result<Self, serde_urlencoded::de::Error> {
|
||||
) -> Result<Self, serde_html_form::de::Error> {
|
||||
let (form, _, _, _) = extract_form_attributes(ev);
|
||||
|
||||
let form_data = web_sys::FormData::new_with_form(&form).unwrap_throw();
|
||||
@@ -570,11 +570,11 @@ where
|
||||
)]
|
||||
fn from_form_data(
|
||||
form_data: &web_sys::FormData,
|
||||
) -> Result<Self, serde_urlencoded::de::Error> {
|
||||
) -> Result<Self, serde_html_form::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_urlencoded::from_str::<Self>(&data)
|
||||
serde_html_form::from_str::<Self>(&data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use leptos::{leptos_dom::HydrationCtx, *};
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
cmp::Reverse,
|
||||
collections::HashMap,
|
||||
ops::IndexMut,
|
||||
rc::Rc,
|
||||
};
|
||||
@@ -33,20 +34,25 @@ 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();
|
||||
|
||||
Branches::initialize(&base.unwrap_or_default(), children(cx));
|
||||
let base_route = router.base();
|
||||
let base = base.unwrap_or_default();
|
||||
|
||||
Branches::initialize(&base, children(cx));
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
if let Some(context) = use_context::<crate::PossibleBranchContext>(cx) {
|
||||
Branches::with(|branches| *context.0.borrow_mut() = branches.to_vec());
|
||||
Branches::with(&base, |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, &router, current_route, &root_equal);
|
||||
let route_states =
|
||||
route_states(cx, base, &router, current_route, &root_equal);
|
||||
|
||||
let id = HydrationCtx::id();
|
||||
let root = root_route(cx, base_route, route_states, root_equal);
|
||||
@@ -103,13 +109,17 @@ 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();
|
||||
|
||||
Branches::initialize(&base.unwrap_or_default(), children(cx));
|
||||
let base_route = router.base();
|
||||
let base = base.unwrap_or_default();
|
||||
|
||||
Branches::initialize(&base, children(cx));
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
if let Some(context) = use_context::<crate::PossibleBranchContext>(cx) {
|
||||
Branches::with(|branches| *context.0.borrow_mut() = branches.to_vec());
|
||||
Branches::with(&base, |branches| {
|
||||
*context.0.borrow_mut() = branches.to_vec()
|
||||
});
|
||||
}
|
||||
|
||||
let animation = Animation {
|
||||
@@ -128,13 +138,16 @@ 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(get_route_matches);
|
||||
let matches = get_route_matches(next_route.clone());
|
||||
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 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());
|
||||
@@ -162,7 +175,8 @@ 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, &router, current_route, &root_equal);
|
||||
let route_states =
|
||||
route_states(cx, base, &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);
|
||||
@@ -211,14 +225,14 @@ pub fn AnimatedRoutes(
|
||||
pub(crate) struct Branches;
|
||||
|
||||
thread_local! {
|
||||
static BRANCHES: RefCell<Option<Vec<Branch>>> = RefCell::new(None);
|
||||
static BRANCHES: RefCell<HashMap<String, Vec<Branch>>> = RefCell::new(HashMap::new());
|
||||
}
|
||||
|
||||
impl Branches {
|
||||
pub fn initialize(base: &str, children: Fragment) {
|
||||
BRANCHES.with(|branches| {
|
||||
let mut current = branches.borrow_mut();
|
||||
if current.is_none() {
|
||||
if !current.contains_key(base) {
|
||||
let mut branches = Vec::new();
|
||||
let children = children
|
||||
.as_children()
|
||||
@@ -246,15 +260,15 @@ impl Branches {
|
||||
&mut Vec::new(),
|
||||
&mut branches,
|
||||
);
|
||||
*current = Some(branches);
|
||||
current.insert(base.to_string(), branches);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with<T>(cb: impl FnOnce(&[Branch]) -> T) -> T {
|
||||
pub fn with<T>(base: &str, cb: impl FnOnce(&[Branch]) -> T) -> T {
|
||||
BRANCHES.with(|branches| {
|
||||
let branches = branches.borrow();
|
||||
let branches = branches.as_ref().expect(
|
||||
let branches = branches.get(base).expect(
|
||||
"Branches::initialize() should be called before \
|
||||
Branches::with()",
|
||||
);
|
||||
@@ -265,13 +279,14 @@ impl 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(current_route.get()));
|
||||
create_memo(cx, move |_| get_route_matches(&base, 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use linear_map::LinearMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{rc::Rc, str::FromStr};
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
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 + 'static,
|
||||
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
fn into_param(
|
||||
value: Option<&str>,
|
||||
@@ -133,10 +133,7 @@ where
|
||||
None => Ok(None),
|
||||
Some(value) => match T::from_str(value) {
|
||||
Ok(value) => Ok(Some(value)),
|
||||
Err(e) => {
|
||||
eprintln!("{e}");
|
||||
Err(ParamsError::Params(Rc::new(e)))
|
||||
}
|
||||
Err(e) => Err(ParamsError::Params(Arc::new(e))),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -154,7 +151,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(Rc::new(e)))
|
||||
Self::from_str(value).map_err(|e| ParamsError::Params(Arc::new(e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,7 +165,7 @@ pub enum ParamsError {
|
||||
MissingParam(String),
|
||||
/// Something went wrong while deserializing a field.
|
||||
#[error("failed to deserialize parameters")]
|
||||
Params(Rc<dyn std::error::Error>),
|
||||
Params(Arc<dyn std::error::Error + Send + Sync>),
|
||||
}
|
||||
|
||||
impl PartialEq for ParamsError {
|
||||
|
||||
@@ -16,7 +16,10 @@ pub(crate) struct RouteMatch {
|
||||
pub route: RouteData,
|
||||
}
|
||||
|
||||
pub(crate) fn get_route_matches(location: String) -> Rc<Vec<RouteMatch>> {
|
||||
pub(crate) fn get_route_matches(
|
||||
base: &str,
|
||||
location: String,
|
||||
) -> Rc<Vec<RouteMatch>> {
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
use lru::LruCache;
|
||||
@@ -28,17 +31,17 @@ pub(crate) fn get_route_matches(location: String) -> Rc<Vec<RouteMatch>> {
|
||||
ROUTE_MATCH_CACHE.with(|cache| {
|
||||
let mut cache = cache.borrow_mut();
|
||||
Rc::clone(cache.get_or_insert(location.clone(), || {
|
||||
build_route_matches(location)
|
||||
build_route_matches(base, location)
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
build_route_matches(location)
|
||||
build_route_matches(base, location)
|
||||
}
|
||||
|
||||
fn build_route_matches(location: String) -> Rc<Vec<RouteMatch>> {
|
||||
Rc::new(Branches::with(|branches| {
|
||||
fn build_route_matches(base: &str, location: String) -> Rc<Vec<RouteMatch>> {
|
||||
Rc::new(Branches::with(base, |branches| {
|
||||
for branch in branches {
|
||||
if let Some(matches) = branch.matcher(&location) {
|
||||
return matches;
|
||||
|
||||
@@ -11,7 +11,7 @@ readme = "../README.md"
|
||||
[dependencies]
|
||||
server_fn_macro_default = { workspace = true }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_urlencoded = "0.7"
|
||||
serde_html_form = "0.2"
|
||||
thiserror = "1"
|
||||
serde_json = "1"
|
||||
quote = "1"
|
||||
|
||||
@@ -1,64 +1,65 @@
|
||||
#![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 it’s registered
|
||||
/// (e.g., `"/api"`). Defaults to `"/"`.
|
||||
/// 3. *Optional*: either `"Cbor"` (specifying that it should use the binary `cbor` format for
|
||||
/// serialization) or `"Url"` (specifying that it should be use a URL-encoded form-data string).
|
||||
/// Defaults to `"Url"`. If you want to use this server function to power 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 client’s perspective it involves an asynchronous
|
||||
/// function call.
|
||||
/// - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
|
||||
/// inside the function body can’t fail, the processes of serialization/deserialization and the
|
||||
/// network call are fallible.
|
||||
/// - **Return types must 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(),
|
||||
}
|
||||
}
|
||||
#![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 it’s 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 client’s perspective it involves an asynchronous
|
||||
/// function call.
|
||||
/// - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
|
||||
/// inside the function body can’t fail, the processes of serialization/deserialization and the
|
||||
/// network call are fallible.
|
||||
/// - **Return types must 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_html_form`](https://docs.rs/serde_html_form/latest/serde_html_form/) 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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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_urlencoded`](https://docs.rs/serde_urlencoded/latest/serde_urlencoded/) or as `application/cbor`
|
||||
//! form data using [`serde_html_form`](https://docs.rs/serde_html_form/latest/serde_html_form/) 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_urlencoded::from_bytes(data).map_err(|e| {
|
||||
serde_html_form::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 results {0}")]
|
||||
#[error("error serializing server function arguments {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_urlencoded::to_string(&args)
|
||||
serde_html_form::to_string(&args)
|
||||
.map_err(|e| ServerFnError::Serialization(e.to_string()))?,
|
||||
),
|
||||
Encoding::Cbor => {
|
||||
|
||||
Reference in New Issue
Block a user