Compare commits

..

13 Commits

Author SHA1 Message Date
Greg Johnston
8f8b6dc440 cargo fmt 2023-05-03 08:49:12 -04:00
Greg Johnston
af561abdf8 fix: suppress warning when loading local resource not under <Suspense/> in hydrate mode 2023-05-03 08:48:20 -04:00
agilarity
fcb98474b8 examples: fix the lint issues in the counter example (#971) 2023-05-01 17:27:29 -04:00
Greg Johnston
54f7e9366a change/fix: require FromStr errors on Params to be Send + Sync so they are ErrorBoundary compatible (#974) 2023-05-01 17:18:46 -04:00
Matt Crane
ddf9df2b5e change: replace serde_urlencoded with serde_html_form to support Vec<_> in server fn args (#973) 2023-05-01 17:17:45 -04:00
Greg Johnston
7fe9f82d89 v0.3.0-alpha (#968) 2023-04-28 19:30:16 -04:00
Roland Fredenhagen
661adc4027 feat: ```view code block in doc comments for properties (#961) 2023-04-28 16:03:04 -04:00
Roland Fredenhagen
1011c464dc feat: add collect_view(cx) (#956) 2023-04-28 16:02:24 -04:00
Frank Panetta
4b498a3b42 chore: fix typos (#964) 2023-04-28 12:10:48 -04:00
yuuma03
3c90b47e77 fix: allow mounting multiple Leptos apps on same server (#966)
Use a HashMap indexed by base URL to cache route branches on the server.
2023-04-28 12:10:02 -04:00
Greg Johnston
671b1e4a8f docs: note need for serde dependency for server functions (closes #947) (#962) 2023-04-27 17:15:29 -04:00
agilarity
52021be806 tests: add wasm web test and common tasks (#954)
* test: rename web test module

* test: extract wasm-web-test task

* test: introduce common tasks

* test: add web-test and common tasks
2023-04-27 17:00:13 -04:00
Roland Fredenhagen
75a7bd610a fix: escapes in doc comments on component properties (#958) 2023-04-27 16:43:38 -04:00
46 changed files with 505 additions and 272 deletions

View File

@@ -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

View File

@@ -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"

View File

@@ -31,7 +31,7 @@ where
This is pretty straightforward: when the user is logged in, we want to show `children`. Until if the user is not logged in, we want to show `fallback`. And while were waiting to find out, we just render `()`, i.e., nothing.
In other words, we want to pass the children of `<WhenLoaded>/` _through_ the `<Suspense/>` component to become the children of the `<Show/>`. This is what I mean by “projection.”
In other words, we want to pass the children of `<WhenLoaded/>` _through_ the `<Suspense/>` component to become the children of the `<Show/>`. This is what I mean by “projection.”
This wont compile.
@@ -40,7 +40,7 @@ error[E0507]: cannot move out of `fallback`, a captured variable in an `Fn` clos
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 names. 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 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
@@ -80,13 +80,13 @@ Suspense(
)
```
All components own their props; so the `<Show/>` in this case cant be called, because it only has captured references to `fallback` and `children`.
All components own their props; so the `<Show/>` in this case cant be called because it only has captured references to `fallback` and `children`.
## Solution
However, both `<Suspense/>` and `<Show/>` take `ChildrenFn`, i.e., their `children` should implement the `Fn` type so they can be called multiple times with only an immutable reference. This means we dont need to own `children` or `fallback`; we just need to be able to pass `'static` references to them.
We can solve this problem by using the [`store_value`](https://docs.rs/leptos/latest/leptos/fn.store_value.html) primitive. This essentially stores a value in the reactive system, handing ownership off to the framework in exchange for a reference that is, like signals, `Copy` and `'static`, and which we can access or modify through certain methods.
We can solve this problem by using the [`store_value`](https://docs.rs/leptos/latest/leptos/fn.store_value.html) primitive. This essentially stores a value in the reactive system, handing ownership off to the framework in exchange for a reference that is, like signals, `Copy` and `'static`, which we can access or modify through certain methods.
In this case, its really simple:
@@ -113,7 +113,7 @@ where
}
```
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.
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

View File

@@ -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)
}
```

View File

@@ -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 doesnt 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>

View File

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

View File

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

View File

@@ -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",
@@ -24,7 +23,6 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
"ssr_modes_axum",
"tailwind",
"tailwind_csr_trunk",
"timer",
"todo_app_sqlite",
"todo_app_sqlite_axum",
"todo_app_sqlite_viz",
@@ -43,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]

View 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"]

View File

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

View File

@@ -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"]

View File

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

View File

@@ -4,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
/>
}
})
}

View File

@@ -9,7 +9,7 @@ wasm_bindgen_test_configure!(run_in_browser);
fn clear() {
let document = leptos::document();
let test_wrapper = document.create_element("section").unwrap();
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()

View File

@@ -1,12 +1,7 @@
[env]
CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome"
[tasks.test-all]
dependencies = ["test", "web-test"]
[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"

View File

@@ -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"]

View File

@@ -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>");
}

View 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>"
);
}

View File

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

View File

@@ -55,7 +55,7 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
errors
.iter()
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li> })
.collect::<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)
})
})
};

View File

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

View File

@@ -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! {

View File

@@ -44,7 +44,7 @@ fn HomePage(cx: Scope) -> impl IntoView {
.map(|posts| {
posts.iter()
.map(|post| view! { cx, <li><a href=format!("/post/{}", post.id)>{&post.title}</a></li>})
.collect::<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>

View File

@@ -49,7 +49,7 @@ fn HomePage(cx: Scope) -> impl IntoView {
.map(|posts| {
posts.iter()
.map(|post| view! { cx, <li><a href=format!("/post/{}", post.id)>{&post.title}</a> "|" <a href=format!("/post_in_order/{}", post.id)>{&post.title}"(in order)"</a></li>})
.collect::<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>

View File

@@ -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! {

View File

@@ -229,8 +229,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
</li>
}
})
.collect::<Vec<_>>()
.into_view(cx)
.collect_view(cx)
}
}
})
@@ -249,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! {

View File

@@ -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! {

View File

@@ -164,8 +164,8 @@ pub use leptos_dom::{
window_event_listener_with_precast,
},
html, log, math, mount_to, mount_to_body, svg, warn, window, Attribute,
Class, 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::*;

View File

@@ -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),
/// })
/// }
/// }

View File

@@ -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),
/// })
/// }
/// }

View File

@@ -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.

View 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]

View 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!()
}

View File

@@ -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 {
@@ -291,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);
@@ -307,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)
}
@@ -320,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}")

View File

@@ -816,7 +816,7 @@ 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`
/// 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

View File

@@ -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<()>| {

View File

@@ -72,7 +72,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/). **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

View File

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

View File

@@ -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" }

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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"

View File

@@ -49,7 +49,7 @@ use syn::__private::ToTokens;
/// - **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`
/// 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 {

View File

@@ -75,7 +75,7 @@
//! This should be fairly obvious: we have to serialize arguments to send them to the server, and we
//! need to deserialize the result to return it to the client.
//! - **Arguments must be implement [serde::Serialize].** They are serialized as an `application/x-www-form-urlencoded`
//! form data using [`serde_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())
})
}
@@ -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 => {