Compare commits

...

36 Commits
0.6.2 ... 2225

Author SHA1 Message Date
Greg Johnston
d577aadddd remove warning in release mode 2024-02-09 19:29:14 -05:00
Greg Johnston
6d5e08af51 docs: give a warning when you try to .dispatch() an action immediately (closes #2225) 2024-02-09 19:29:14 -05:00
Greg Johnston
8a77691cb5 Merge pull request #2285 from leptos-rs/fix-issues
Fix remaining CI issues
2024-02-09 19:29:01 -05:00
Greg Johnston
1dbe8b2d4b fix: correct feature name for server-fn-macro crate (broken in #2270) 2024-02-09 17:18:44 -05:00
Greg Johnston
fe64f0d332 examples: fix counter_isomorphic (broken in #2244) and fix additional warnings 2024-02-09 17:12:31 -05:00
Joseph Cruz
c00207aa46 fix(test-report) should show all cargo-make leptos configuration (#2282)
* refactor(test-report): extract script
* refactor(test-report): extract functions
* refactor(test-report): split option to tasks
* chore(test-report): highlight examples without tags
* fix(test-report): show all cargo-make leptos configuration
* docs(test-report): update keys
* chore(test-report): include all crates in report
2024-02-09 16:31:46 -05:00
Joseph Cruz
65b7603192 fix(ci): address clippy issue (#2278)
* fix(ci): address clippy issue
* fix(ci): add missing nightly specifications
* fix(ci):  set all nightly references
* chore(ci): do not lint example crates
2024-02-09 16:30:11 -05:00
haslersn
d4bdc36062 fix: add key/value pair from submit button when parsing form event (#2268) 2024-02-07 11:09:01 -05:00
Sam Judelson
1b55227d10 fix: remove unnecessary default features on axum in server_fns to support running Axum in a WASM environment (#2270) 2024-02-07 11:08:48 -05:00
Saikat Das
a903e19eb2 chore: fix typo (#2267) 2024-02-06 17:55:35 -05:00
blorbb
38bf73947f fix: make directive .into() calls consistent (#2249) 2024-02-05 08:52:12 -05:00
Greg Johnston
e4b89ba243 Merge pull request #2262 from leptos-rs/2261
fix: guarantee execution order of effects (closes #2261)
2024-02-05 08:51:23 -05:00
Greg Johnston
701e3077fb chore: cargo fmt 2024-02-05 06:38:02 -05:00
Greg Johnston
aec4d680aa fix: guarantee execution order of effects (closes #2261) 2024-02-05 06:35:57 -05:00
Steffen
06721c5fcd examples: fix typos in examples (#2260) 2024-02-05 05:20:53 -05:00
SleeplessOne1917
1ddb39e9bd docs: typo in actix integrations docs (#2258)
Co-authored-by: SleeplessOne1917 <insomnia-void@protonmail.com>
2024-02-04 20:57:51 -05:00
Chris
15d4ca0638 feat(axum): provide state to server fn context (#2257)
Note that this is a minimal implementation and will __not__ allow the
user to `expect_state` if they have external calls to rendering their
app (i.e. using `render_app_to_*` directly).
2024-02-04 19:26:21 -05:00
zoomiti
85c3755f6d fix: bug with percent decoding of url params (#2251) 2024-02-04 19:24:02 -05:00
Sam Judelson
66ea072bc0 docs: a note to HtmlElement<El> about Deref (#2218) 2024-02-04 15:34:39 -05:00
Joris Hartog
b0b3c21285 docs: fix broken link in leptos_router (#2256) 2024-02-04 15:29:34 -05:00
Greg Johnston
56088a9ead fix: error rather than panicking if unable to send response in Axum integration (#2241)
* fix: error rather than panicking if unable to send response in Axum integration
2024-02-03 19:18:41 -05:00
martin frances
69d25d9c63 examples/hackernews: Add a "Suspense" wrapper. (#2253)
This warning appears in the browser's console log.

```
hackernews.js:933 At src/routes/stories.rs:39:17, 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:
1. Wrap the place where you read the resource in a <Suspense/> or <Transition/> component, or
2. 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.)
```
2024-02-03 14:24:46 -08:00
martin frances
5029b8f315 Chore: Minor, ran ``cargo fmt`` (#2254) 2024-02-03 14:24:12 -08:00
martin frances
0cba7bc22b example/counter_isomorphic Removed console warning. (#2244)
This warning is seen in the browsers console window.

```
counter_isomorphic.js:1068 At src/counters.rs:138:17, 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:
1. Wrap the place where you read the resource in a <Suspense/> or <Transition/> component, or
2. 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.)
```

Two derived signals "value" and "error_msg" need to be wrapped in a <Suspense> block.

"value" falls back to just the initial text.
"error" uses the default fallback.
2024-02-01 16:50:15 -08:00
Michael Kadziela
fb97c50886 Update rkyv example button text to accurately reflect what it does (#2250) 2024-02-01 16:49:29 -08:00
Greg Johnston
f1bc734dcf 0.6.5 2024-01-31 19:40:41 -05:00
Greg Johnston
f71b4aae69 feat: easily create custom server fn clients (#2247) 2024-01-31 09:15:30 -05:00
Greg Johnston
a834c03974 fix: bug with Actix redirects (#2246) 2024-01-31 09:14:40 -05:00
Greg Johnston
595013579c 0.6.4 2024-01-30 09:17:52 -05:00
Greg Johnston
8b1bd1ae9e Merge pull request #2240 from leptos-rs/err-serialization
fix: serialization error during SSR on ServerFnError
2024-01-29 16:32:55 -05:00
Greg Johnston
6ef1531059 example: file upload with streaming progress bar (#2242) 2024-01-29 15:20:19 -05:00
Greg Johnston
9f1406250e chore: update deprecated .remove() method on IndexSet 2024-01-29 11:32:15 -05:00
Greg Johnston
1f6a892291 fix: serialization error during SSR on ServerFnError 2024-01-29 10:36:08 -05:00
Greg Johnston
0ff1e279a2 fix: correctly track source in create_local_resource (#2238) 2024-01-28 10:09:03 -08:00
Chris
c6096cc2a0 chore: define edtion = "2021" in rustfmt.toml (#2235) 2024-01-27 16:04:25 -08:00
Greg Johnston
8a2ae7fc7c \v0.6.3\ 2024-01-26 21:00:21 -05:00
89 changed files with 740 additions and 273 deletions

View File

@@ -29,4 +29,4 @@ jobs:
with:
directory: ${{ matrix.directory }}
cargo_make_task: "ci"
toolchain: nightly
toolchain: nightly-2024-01-29

View File

@@ -24,4 +24,4 @@ jobs:
with:
directory: ${{ matrix.directory }}
cargo_make_task: "ci"
toolchain: nightly
toolchain: nightly-2024-01-29

View File

@@ -40,4 +40,4 @@ jobs:
with:
directory: ${{ matrix.directory }}
cargo_make_task: "ci"
toolchain: nightly
toolchain: nightly-2024-01-29

View File

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

View File

@@ -150,7 +150,7 @@ There are several people in the community using Leptos right now for internal ap
### Can I use this for native GUI?
Sure! Obviously the `view` macro is for generating DOM nodes but you can use the reactive system to drive native any GUI toolkit that uses the same kind of object-oriented, event-callback-based framework as the DOM pretty easily. The principles are the same:
Sure! Obviously the `view` macro is for generating DOM nodes but you can use the reactive system to drive any native GUI toolkit that uses the same kind of object-oriented, event-callback-based framework as the DOM pretty easily. The principles are the same:
- Use signals, derived signals, and memos to create your reactive system
- Create GUI widgets

View File

@@ -3,5 +3,5 @@ alias = "check-all"
[tasks.check-all]
command = "cargo"
args = ["+nightly", "check-all-features"]
args = ["+nightly-2024-01-29", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -3,5 +3,5 @@ alias = "test-all"
[tasks.test-all]
command = "cargo"
args = ["+nightly", "test-all-features"]
args = ["+nightly-2024-01-29", "test-all-features"]
install_crate = "cargo-all-features"

View File

@@ -51,103 +51,5 @@ echo "CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = $examples"
[tasks.test-report]
workspace = false
description = "report web testing technology used by examples - OPTION: [all]"
script = '''
set -emu
BOLD="\e[1m"
GREEN="\e[0;32m"
ITALIC="\e[3m"
YELLOW="\e[0;33m"
RESET="\e[0m"
echo
echo "${YELLOW}Web Test Technology${RESET}"
echo
makefile_paths=$(find . -name Makefile.toml -not -path '*/target/*' -not -path '*/node_modules/*' |
sed 's%./%%' |
sed 's%/Makefile.toml%%' |
grep -v Makefile.toml |
sort -u)
start_path=$(pwd)
for path in $makefile_paths; do
cd $path
crate_symbols=
pw_count=$(find . -name playwright.config.ts | wc -l)
while read -r line; do
case $line in
*"cucumber"*)
crate_symbols=$crate_symbols"C"
;;
*"fantoccini"*)
crate_symbols=$crate_symbols"D"
;;
esac
done <"./Cargo.toml"
while read -r line; do
case $line in
*"cargo-make/wasm-test.toml"*)
crate_symbols=$crate_symbols"W"
;;
*"cargo-make/playwright-test.toml"*)
crate_symbols=$crate_symbols"P"
crate_symbols=$crate_symbols"N"
;;
*"cargo-make/playwright-trunk-test.toml"*)
crate_symbols=$crate_symbols"P"
crate_symbols=$crate_symbols"T"
;;
*"cargo-make/trunk_server.toml"*)
crate_symbols=$crate_symbols"T"
;;
*"cargo-make/cargo-leptos-webdriver-test.toml"*)
crate_symbols=$crate_symbols"L"
;;
*"cargo-make/cargo-leptos-test.toml"*)
crate_symbols=$crate_symbols"L"
if [ $pw_count -gt 0 ]; then
crate_symbols=$crate_symbols"P"
fi
;;
esac
done <"./Makefile.toml"
# Sort list of tools
sorted_crate_symbols=$(echo ${crate_symbols} | grep -o . | sort | tr -d "\n")
formatted_crate_symbols="${BOLD}${YELLOW}${sorted_crate_symbols}${RESET}"
crate_line=$path
if [ ! -z ${1+x} ]; then
# Show all examples
if [ ! -z $crate_symbols ]; then
crate_line=$crate_line$formatted_crate_symbols
fi
echo $crate_line
elif [ ! -z $crate_symbols ]; then
# Filter out examples that do not run tests in `ci`
crate_line=$crate_line$formatted_crate_symbols
echo $crate_line
fi
cd ${start_path}
done
c="${BOLD}${YELLOW}C${RESET} = Cucumber"
d="${BOLD}${YELLOW}D${RESET} = WebDriver"
l="${BOLD}${YELLOW}L${RESET} = Cargo Leptos"
n="${BOLD}${YELLOW}N${RESET} = Node"
p="${BOLD}${YELLOW}P${RESET} = Playwright"
t="${BOLD}${YELLOW}T${RESET} = Trunk"
w="${BOLD}${YELLOW}W${RESET} = WASM"
echo
echo "${ITALIC}Keys:${RESET} $c, $d, $l, $n, $p, $t, $w"
echo
'''
description = "show the cargo-make configuration for web examples"
script = { file = "./cargo-make/scripts/web-report.sh" }

View File

@@ -15,13 +15,13 @@ clear = true
dependencies = ["check-debug", "check-release"]
[tasks.check-debug]
toolchain = "nightly"
toolchain = "nightly-2024-01-29"
command = "cargo"
args = ["check-all-features"]
install_crate = "cargo-all-features"
[tasks.check-release]
toolchain = "nightly"
toolchain = "nightly-2024-01-29"
command = "cargo"
args = ["check-all-features", "--release"]
install_crate = "cargo-all-features"

View File

@@ -1,11 +1,11 @@
[tasks.build]
toolchain = "nightly"
toolchain = "nightly-2024-01-29"
command = "cargo"
args = ["build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
toolchain = "nightly"
toolchain = "nightly-2024-01-29"
command = "cargo"
args = ["check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -1,5 +1,5 @@
[tasks.pre-clippy]
env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features -- -D warnings" }
env = { CARGO_MAKE_CLIPPY_ARGS = "--no-deps --all-targets --all-features -- -D warnings" }
[tasks.check-style]
dependencies = ["check-format-flow", "clippy-flow"]

View File

@@ -0,0 +1,154 @@
#!/bin/bash
set -emu
BOLD="\e[1m"
ITALIC="\e[3m"
YELLOW="\e[0;33m"
RESET="\e[0m"
function web { #task: only include examples with web cargo-make configuration
print_header
print_crate_tags "$@"
print_footer
}
function all { #task: includes all examples
print_header
print_crate_tags "all"
print_footer
}
function print_header {
echo -e "${YELLOW}Cargo Make Web Report${RESET}"
echo
echo -e "${ITALIC}Show how crates are configured to run and test web examples with cargo-make${RESET}"
echo
}
function print_crate_tags {
local makefile_paths
makefile_paths=$(find_makefile_lines)
local start_path
start_path=$(pwd)
for path in $makefile_paths; do
cd "$path"
local crate_tags=
# Add cargo tags
while read -r line; do
case $line in
*"cucumber"*)
crate_tags=$crate_tags"C"
;;
*"fantoccini"*)
crate_tags=$crate_tags"F"
;;
esac
done <"./Cargo.toml"
#Add makefile tags
local pw_count
pw_count=$(find . -name playwright.config.ts | wc -l)
while read -r line; do
case $line in
*"cargo-make/wasm-test.toml"*)
crate_tags=$crate_tags"W"
;;
*"cargo-make/playwright-test.toml"*)
crate_tags=$crate_tags"P"
crate_tags=$crate_tags"N"
;;
*"cargo-make/playwright-trunk-test.toml"*)
crate_tags=$crate_tags"P"
crate_tags=$crate_tags"T"
;;
*"cargo-make/trunk_server.toml"*)
crate_tags=$crate_tags"T"
;;
*"cargo-make/cargo-leptos-webdriver-test.toml"*)
crate_tags=$crate_tags"L"
;;
*"cargo-make/cargo-leptos-test.toml"*)
crate_tags=$crate_tags"L"
if [ "$pw_count" -gt 0 ]; then
crate_tags=$crate_tags"P"
fi
;;
*"cargo-make/cargo-leptos.toml"*)
crate_tags=$crate_tags"L"
;;
esac
done <"./Makefile.toml"
# Sort tags
local sorted_crate_symbols
sorted_crate_symbols=$(echo "$crate_tags" | grep -o . | sort | tr -d "\n")
# Maybe print line
local crate_line=$path
if [ -n "$crate_tags" ]; then
crate_line="$crate_line${YELLOW}$sorted_crate_symbols${RESET}"
echo -e "$crate_line"
elif [ "$#" -gt 0 ]; then
crate_line="${BOLD}$crate_line${RESET}"
echo -e "$crate_line"
fi
cd "$start_path"
done
}
function find_makefile_lines {
find . -name Makefile.toml -not -path '*/target/*' -not -path '*/node_modules/*' |
sed 's%./%%' |
sed 's%/Makefile.toml%%' |
grep -v Makefile.toml |
sort -u
}
function print_footer {
c="${BOLD}${YELLOW}C${RESET} = Cucumber Test Runner"
d="${BOLD}${YELLOW}F${RESET} = Fantoccini WebDriver"
l="${BOLD}${YELLOW}L${RESET} = Cargo Leptos"
n="${BOLD}${YELLOW}N${RESET} = Node"
p="${BOLD}${YELLOW}P${RESET} = Playwright Test"
t="${BOLD}${YELLOW}T${RESET} = Trunk"
w="${BOLD}${YELLOW}W${RESET} = WASM Test"
echo
echo -e "${ITALIC}Technology Keys:${RESET}\n $c\n $d\n $l\n $n\n $p\n $t\n $w"
echo
}
###################
# HELP
###################
function list_help_for {
local task=$1
grep -E "^function.+ #$task" "$0" |
sed 's/function/ /' |
sed -e "s| { #$task: |~|g" |
column -s"~" -t |
sort
}
function help { #help: show task descriptions
echo -e "${BOLD}Usage:${RESET} ./$(basename "$0") <task> [options]"
echo
echo "Show the cargo-make configuration for web examples"
echo
echo -e "${BOLD}Tasks:${RESET}"
list_help_for task
echo
}
TIMEFORMAT="./web-report.sh completed in %3lR"
time "${@:-all}" # Show the report by default

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "nightly-2024-01-29"

View File

@@ -69,7 +69,7 @@ reload-port = 3001
end2end-cmd = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"

View File

@@ -132,15 +132,6 @@ pub fn Counter() -> impl IntoView {
|_| get_server_count(),
);
let value =
move || counter.get().map(|count| count.unwrap_or(0)).unwrap_or(0);
let error_msg = move || {
counter.get().and_then(|res| match res {
Ok(_) => None,
Err(e) => Some(e),
})
};
view! {
<div>
<h2>"Simple Counter"</h2>
@@ -150,15 +141,24 @@ pub fn Counter() -> impl IntoView {
<div>
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
<button on:click=move |_| dec.dispatch(())>"-1"</button>
<span>"Value: " {value} "!"</span>
<span>
"Value: "
<Suspense>
{move || counter.and_then(|count| *count)} "!"
</Suspense>
</span>
<button on:click=move |_| inc.dispatch(())>"+1"</button>
</div>
{move || {
error_msg()
.map(|msg| {
view! { <p>"Error: " {msg.to_string()}</p> }
})
}}
<Suspense>
{move || {
counter.get().and_then(|res| match res {
Ok(_) => None,
Err(e) => Some(e),
}).map(|msg| {
view! { <p>"Error: " {msg.to_string()}</p> }
})
}}
</Suspense>
</div>
}
}
@@ -204,7 +204,7 @@ pub fn FormCounter() -> impl IntoView {
<input type="hidden" name="msg" value="form value down"/>
<input type="submit" value="-1"/>
</ActionForm>
<span>"Value: " {move || value().to_string()} "!"</span>
<span>"Value: " <Suspense>{move || value().to_string()} "!"</Suspense></span>
<ActionForm action=adjust>
<input type="hidden" name="delta" value="1"/>
<input type="hidden" name="msg" value="form value up"/>

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "nightly-2024-01-29"

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "nightly-2024-01-29"

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "nightly-2024-01-29"

View File

@@ -1,5 +1,6 @@
use leptos::{ev::click, html::AnyElement, *};
// no extra parameter
pub fn highlight(el: HtmlElement<AnyElement>) {
let mut highlighted = false;
@@ -14,6 +15,7 @@ pub fn highlight(el: HtmlElement<AnyElement>) {
});
}
// one extra parameter
pub fn copy_to_clipboard(el: HtmlElement<AnyElement>, content: &str) {
let content = content.to_string();
@@ -31,6 +33,35 @@ pub fn copy_to_clipboard(el: HtmlElement<AnyElement>, content: &str) {
});
}
// custom parameter
#[derive(Clone)]
pub struct Amount(usize);
impl From<usize> for Amount {
fn from(value: usize) -> Self {
Self(value)
}
}
// a 'default' value if no value is passed in
impl From<()> for Amount {
fn from(_: ()) -> Self {
Self(1)
}
}
// .into() will automatically be called on the parameter
pub fn add_dot(el: HtmlElement<AnyElement>, amount: Amount) {
_ = el.clone().on(click, move |_| {
el.set_inner_text(&format!(
"{}{}",
el.inner_text(),
".".repeat(amount.0)
))
})
}
#[component]
pub fn SomeComponent() -> impl IntoView {
view! {
@@ -46,6 +77,11 @@ pub fn App() -> impl IntoView {
view! {
<a href="#" use:copy_to_clipboard=data>"Copy \"" {data} "\" to clipboard"</a>
// automatically applies the directive to every root element in `SomeComponent`
<SomeComponent use:highlight />
// no value will default to `().into()`
<button use:add_dot>"Add a dot"</button>
// `5.into()` automatically called
<button use:add_dot=5>"Add 5 dots"</button>
}
}

View File

@@ -8,7 +8,7 @@ See the [Examples README](../README.md) for setup and run instructions.
## Testing
This project is configured to run start and stop of processes for integration tests wihtout the use of Cargo Leptos or Node.
This project is configured to run start and stop of processes for integration tests without the use of Cargo Leptos or Node.
## Quick Start

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "nightly-2024-01-29"

View File

@@ -61,7 +61,7 @@ reload-port = 3001
end2end-cmd = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "nightly-2024-01-29"

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "nightly-2024-01-29"

View File

@@ -70,7 +70,7 @@ reload-port = 3001
end2end-cmd = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "nightly-2024-01-29"

View File

@@ -62,16 +62,18 @@ pub fn Stories() -> impl IntoView {
}}
</span>
<span>"page " {page}</span>
<span class="page-link"
class:disabled=hide_more_link
aria-hidden=hide_more_link
>
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
aria-label="Next Page"
<Suspense>
<span class="page-link"
class:disabled=hide_more_link
aria-hidden=hide_more_link
>
"more >"
</a>
</span>
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
aria-label="Next Page"
>
"more >"
</a>
</span>
</Suspense>
</div>
<main class="news-list">
<div>

View File

@@ -71,7 +71,7 @@ reload-port = 3001
end2end-cmd = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "nightly-2024-01-29"

View File

@@ -81,7 +81,7 @@ reload-port = 3001
end2end-cmd = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "nightly-2024-01-29"

View File

@@ -78,7 +78,7 @@ reload-port = 3001
end2end-cmd = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"

View File

@@ -5,13 +5,13 @@ extend = [
]
[tasks.build]
toolchain = "nightly"
toolchain = "nightly-2024-01-29"
command = "cargo"
args = ["build-all-features", "--target", "wasm32-unknown-unknown"]
install_crate = "cargo-all-features"
[tasks.check]
toolchain = "nightly"
toolchain = "nightly-2024-01-29"
command = "cargo"
args = ["check-all-features", "--target", "wasm32-unknown-unknown"]
install_crate = "cargo-all-features"

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "nightly-2024-01-29"

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "nightly-2024-01-29"

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "nightly-2024-01-29"

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "nightly-2024-01-29"

View File

@@ -15,7 +15,7 @@ leptos = { path = "../../leptos", features = ["nightly"] }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_meta = { path = "../../meta", features = ["nightly"] }
leptos_router = { path = "../../router", features = ["nightly"] }
server_fn = { path = "../../server_fn", features = ["serde-lite", "rkyv", "multipart" ]}
server_fn = { path = "../../server_fn", features = ["serde-lite", "rkyv", "multipart"] }
log = "0.4"
simple_logger = "4.0"
serde = { version = "1", features = ["derive"] }
@@ -31,6 +31,9 @@ web-sys = { version = "0.3.67", features = ["FileList", "File"] }
strum = { version = "0.25.0", features = ["strum_macros", "derive"] }
notify = { version = "6.1.1", optional = true }
pin-project-lite = "0.2.13"
dashmap = { version = "5.5.3", optional = true }
once_cell = { version = "1.19.0", optional = true }
async-broadcast = { version = "0.6.0", optional = true }
[features]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
@@ -43,7 +46,10 @@ ssr = [
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:leptos_axum",
"dep:notify"
"dep:notify",
"dep:dashmap",
"dep:once_cell",
"dep:async-broadcast",
]
[package.metadata.cargo-all-features]
@@ -71,7 +77,7 @@ end2end-cmd = "cargo make test-ui"
end2end-dir = "e2e"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "nightly-2024-01-29"

View File

@@ -5,13 +5,15 @@ use leptos_meta::{provide_meta_context, Link, Meta, Stylesheet};
use leptos_router::{ActionForm, Route, Router, Routes};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use server_fn::{
client::{browser::BrowserClient, Client},
codec::{
Encoding, FromReq, FromRes, GetUrl, IntoReq, IntoRes, MultipartData,
MultipartFormData, Rkyv, SerdeLite, StreamingText, TextStream,
},
request::{ClientReq, Req},
response::{ClientRes, Res},
request::{browser::BrowserRequest, ClientReq, Req},
response::{browser::BrowserResponse, ClientRes, Res},
};
use std::future::Future;
#[cfg(feature = "ssr")]
use std::sync::{
atomic::{AtomicU8, Ordering},
@@ -55,8 +57,10 @@ pub fn HomePage() -> impl IntoView {
<ServerFnArgumentExample/>
<RkyvExample/>
<FileUpload/>
<FileUploadWithProgress/>
<FileWatcher/>
<CustomEncoding/>
<CustomClientExample/>
}
}
@@ -316,7 +320,7 @@ pub fn RkyvExample() -> impl IntoView {
set_input(value);
}
>
Click to see length
Click to capitalize
</button>
<p>{input}</p>
<Transition>
@@ -331,8 +335,8 @@ pub fn FileUpload() -> impl IntoView {
///
/// On the server, this uses the `multer` crate, which provides a streaming API.
#[server(
input = MultipartFormData,
)]
input = MultipartFormData,
)]
pub async fn file_length(
data: MultipartData,
) -> Result<usize, ServerFnError> {
@@ -390,6 +394,168 @@ pub fn FileUpload() -> impl IntoView {
}
}
/// This component uses server functions to upload a file, while streaming updates on the upload
/// progress.
#[component]
pub fn FileUploadWithProgress() -> impl IntoView {
/// In theory, you could create a single server function which
/// 1) received multipart form data
/// 2) returned a stream that contained updates on the progress
///
/// In reality, browsers do not actually support duplexing requests in this way. In other
/// words, every existing browser actually requires that the request stream be complete before
/// it begins processing the response stream.
///
/// Instead, we can create two separate server functions:
/// 1) one that receives multipart form data and begins processing the upload
/// 2) a second that returns a stream of updates on the progress
///
/// This requires us to store some global state of all the uploads. In a real app, you probably
/// shouldn't do exactly what I'm doing here in the demo. For example, this map just
/// distinguishes between files by filename, not by user.
#[cfg(feature = "ssr")]
mod progress {
use async_broadcast::{broadcast, Receiver, Sender};
use dashmap::DashMap;
use futures::Stream;
use once_cell::sync::Lazy;
struct File {
total: usize,
tx: Sender<usize>,
rx: Receiver<usize>,
}
static FILES: Lazy<DashMap<String, File>> = Lazy::new(DashMap::new);
pub async fn add_chunk(filename: &str, len: usize) {
println!("[{filename}]\tadding {len}");
let mut entry =
FILES.entry(filename.to_string()).or_insert_with(|| {
println!("[{filename}]\tinserting channel");
let (tx, rx) = broadcast(128);
File { total: 0, tx, rx }
});
entry.total += len;
let new_total = entry.total;
// we're about to do an async broadcast, so we don't want to hold a lock across it
let tx = entry.tx.clone();
drop(entry);
// now we send the message and don't have to worry about it
tx.broadcast(new_total)
.await
.expect("couldn't send a message over channel");
}
pub fn for_file(filename: &str) -> impl Stream<Item = usize> {
let entry =
FILES.entry(filename.to_string()).or_insert_with(|| {
println!("[{filename}]\tinserting channel");
let (tx, rx) = broadcast(128);
File { total: 0, tx, rx }
});
entry.rx.clone()
}
}
#[server(
input = MultipartFormData,
)]
pub async fn upload_file(data: MultipartData) -> Result<(), ServerFnError> {
let mut data = data.into_inner().unwrap();
while let Ok(Some(mut field)) = data.next_field().await {
let name =
field.file_name().expect("no filename on field").to_string();
while let Ok(Some(chunk)) = field.chunk().await {
let len = chunk.len();
println!("[{name}]\t{len}");
progress::add_chunk(&name, len).await;
// in a real server function, you'd do something like saving the file here
}
}
Ok(())
}
#[server(output = StreamingText)]
pub async fn file_progress(
filename: String,
) -> Result<TextStream, ServerFnError> {
println!("getting progress on {filename}");
// get the stream of current length for the file
let progress = progress::for_file(&filename);
// separate each number with a newline
// the HTTP response might pack multiple lines of this into a single chunk
// we need some way of dividing them up
let progress = progress.map(|bytes| Ok(format!("{bytes}\n")));
Ok(TextStream::new(progress))
}
let (filename, set_filename) = create_signal(None);
let (max, set_max) = create_signal(None);
let (current, set_current) = create_signal(None);
let on_submit = move |ev: SubmitEvent| {
ev.prevent_default();
let target = ev.target().unwrap().unchecked_into::<HtmlFormElement>();
let form_data = FormData::new_with_form(&target).unwrap();
let file = form_data
.get("file_to_upload")
.unchecked_into::<web_sys::File>();
let filename = file.name();
let size = file.size() as usize;
set_filename(Some(filename.clone()));
set_max(Some(size));
set_current(None::<usize>);
spawn_local(async move {
let mut progress = file_progress(filename)
.await
.expect("couldn't initialize stream")
.into_inner();
while let Some(Ok(len)) = progress.next().await {
// the TextStream from the server function will be a series of `usize` values
// however, the response itself may pack those chunks into a smaller number of
// chunks, each with more text in it
// so we've padded them with newspace, and will split them out here
// each value is the latest total, so we'll just take the last one
let len = len
.split('\n')
.filter(|n| !n.is_empty())
.last()
.expect(
"expected at least one non-empty value from \
newline-delimited rows",
)
.parse::<usize>()
.expect("invalid length");
set_current(Some(len));
}
});
spawn_local(async move {
upload_file(form_data.into())
.await
.expect("couldn't upload file");
});
};
view! {
<h3>File Upload with Progress</h3>
<p>A file upload with progress can be handled with two separate server functions.</p>
<aside>See the doc comment on the component for an explanation.</aside>
<form on:submit=on_submit>
<input type="file" name="file_to_upload"/>
<input type="submit"/>
</form>
{move || filename().map(|filename| view! { <p>Uploading {filename}</p> })}
{move || max().map(|max| view! {
<progress max=max value=move || current().unwrap_or_default()/>
})}
}
}
#[component]
pub fn FileWatcher() -> impl IntoView {
#[server(input = GetUrl, output = StreamingText)]
@@ -632,3 +798,55 @@ pub fn CustomEncoding() -> impl IntoView {
<p>{result}</p>
}
}
/// Middleware lets you modify the request/response on the server.
///
/// On the client, you might also want to modify the request. For example, you may need to add a
/// custom header for authentication on every request. You can do this by creating a "custom
/// client."
#[component]
pub fn CustomClientExample() -> impl IntoView {
// Define a type for our client.
pub struct CustomClient;
// Implement the `Client` trait for it.
impl<CustErr> Client<CustErr> for CustomClient {
// BrowserRequest and BrowserResponse are the defaults used by other server functions.
// They are wrappers for the underlying Web Fetch API types.
type Request = BrowserRequest;
type Response = BrowserResponse;
// Our custom `send()` implementation does all the work.
fn send(
req: Self::Request,
) -> impl Future<Output = Result<Self::Response, ServerFnError<CustErr>>>
+ Send {
// BrowserRequest derefs to the underlying Request type from gloo-net,
// so we can get access to the headers here
let headers = req.headers();
// modify the headers by appending one
headers.append("X-Custom-Header", "foobar");
// delegate back out to BrowserClient to send the modified request
BrowserClient::send(req)
}
}
// Specify our custom client with `client = `
#[server(client = CustomClient)]
pub async fn fn_with_custom_client() -> Result<(), ServerFnError> {
use http::header::HeaderMap;
use leptos_axum::extract;
let headers: HeaderMap = extract().await?;
let custom_header = headers.get("X-Custom-Header");
println!("X-Custom-Header = {custom_header:?}");
Ok(())
}
view! {
<h3>Custom clients</h3>
<p>You can define a custom server function client to do something like adding a header to every request.</p>
<p>Check the network request in your browser devtools to see how this client adds a custom header.</p>
<button on:click=|_| spawn_local(async { fn_with_custom_client().await.unwrap() })>Click me</button>
}
}

View File

@@ -19,7 +19,7 @@ leptos_router = { path = "../../router", features = ["nightly"] }
log = "0.4"
simple_logger = "4.0"
serde = { version = "1.0", features = ["derive"] }
axum = { version = "0.7", optional = true, features=["macros"] }
axum = { version = "0.7", optional = true, features = ["macros"] }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = ["fs"], optional = true }
tokio = { version = "1", features = ["full"], optional = true }
@@ -83,7 +83,7 @@ reload-port = 3001
end2end-cmd = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "nightly-2024-01-29"

View File

@@ -51,7 +51,7 @@ pub mod ssr {
.await
.ok()?;
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
//lets just get all the tokens the user can use, we will only use the full permissions if modifying them.
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
"SELECT token FROM user_permissions WHERE user_id = ?;",
)
@@ -75,7 +75,7 @@ pub mod ssr {
.await
.ok()?;
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
//lets just get all the tokens the user can use, we will only use the full permissions if modifying them.
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
"SELECT token FROM user_permissions WHERE user_id = ?;",
)

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "nightly-2024-01-29"

View File

@@ -1,6 +1,6 @@
use leptos::*;
// Slots are created in simillar manner to components, except that they use the #[slot] macro.
// Slots are created in similar manner to components, except that they use the #[slot] macro.
#[slot]
struct Then {
children: ChildrenFn,

View File

@@ -86,7 +86,7 @@ reload-port = 3001
end2end-cmd = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"

View File

@@ -47,7 +47,7 @@ pub mod ssr_imports {
.await
.ok()?;
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
//lets just get all the tokens the user can use, we will only use the full permissions if modifying them.
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
"SELECT token FROM user_permissions WHERE user_id = ?;",
)
@@ -71,7 +71,7 @@ pub mod ssr_imports {
.await
.ok()?;
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
//lets just get all the tokens the user can use, we will only use the full permissions if modifying them.
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
"SELECT token FROM user_permissions WHERE user_id = ?;",
)

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "nightly-2024-01-29"

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "nightly-2024-01-29"

View File

@@ -8,7 +8,6 @@ crate-type = ["cdylib", "rlib"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
leptos = { path = "../../leptos", features = ["nightly"] }
leptos_actix = { path = "../../integrations/actix", optional = true }
@@ -17,12 +16,12 @@ leptos_router = { path = "../../router", features = ["nightly"] }
gloo-net = { version = "0.2", features = ["http"] }
log = "0.4"
# dependecies for client (enable when csr or hydrate set)
# dependencies for client (enable when csr or hydrate set)
wasm-bindgen = { version = "0.2", optional = true }
console_log = { version = "1", optional = true }
console_error_panic_hook = { version = "0.1", optional = true }
# dependecies for server (enable when ssr set)
# dependencies for server (enable when ssr set)
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", features = ["macros"], optional = true }
futures = { version = "0.3", optional = true }
@@ -97,7 +96,7 @@ end2end-cmd = "npx playwright test"
end2end-dir = "end2end"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "nightly-2024-01-29"

View File

@@ -1,4 +1,4 @@
# Leptos with Axum + TailwindCSS Tempate
# Leptos with Axum + TailwindCSS Template
This is a template demonstrating how to integrate [TailwindCSS](https://tailwindcss.com/) with the [Leptos](https://github.com/leptos-rs/leptos) web framework, Axum server, and the [cargo-leptos](https://github.com/akesson/cargo-leptos) tool.

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "nightly-2024-01-29"

View File

@@ -10,9 +10,7 @@ leptos_router = { path = "../../router", features = ["csr", "nightly"] }
log = "0.4"
gloo-net = { version = "0.2", features = ["http"] }
# dependecies for client (enable when csr or hydrate set)
# dependencies for client (enable when csr or hydrate set)
wasm-bindgen = { version = "0.2" }
console_log = { version = "1"}
console_error_panic_hook = { version = "0.1"}
console_log = { version = "1" }
console_error_panic_hook = { version = "0.1" }

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "nightly-2024-01-29"

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "nightly-2024-01-29"

View File

@@ -67,7 +67,7 @@ end2end-cmd = "cargo make test-ui"
end2end-dir = "e2e"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "nightly-2024-01-29"

View File

@@ -69,7 +69,7 @@ end2end-cmd = "cargo make test-ui"
end2end-dir = "e2e"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "nightly-2024-01-29"

View File

@@ -70,7 +70,7 @@ end2end-cmd = "cargo make test-ui"
end2end-dir = "e2e"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "nightly-2024-01-29"

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "nightly-2024-01-29"

View File

@@ -150,14 +150,6 @@ pub fn redirect(path: &str) {
to redirect()."
);
}
if let Some(response_options) = use_context::<ResponseOptions>() {
response_options.set_status(StatusCode::FOUND);
response_options.insert_header(
header::LOCATION,
header::HeaderValue::from_str(path)
.expect("Failed to create HeaderValue"),
);
}
}
/// An Actix [struct@Route](actix_web::Route) that listens for a `POST` request with
@@ -1369,7 +1361,7 @@ impl LeptosRoutes for &mut ServiceConfig {
}
}
/// A helper to make it easier to use Axum extractors in server functions.
/// A helper to make it easier to use Actix extractors in server functions.
///
/// It is generic over some type `T` that implements [`FromRequest`] and can
/// therefore be used in an extractor. The compiler can often infer this type.

View File

@@ -14,7 +14,7 @@ axum = { version = "0.7", default-features = false, features = [
futures = "0.3"
http-body-util = "0.1"
leptos = { workspace = true, features = ["ssr"] }
server_fn = { workspace = true, features = ["axum"] }
server_fn = { workspace = true, features = ["axum-no-default"] }
leptos_macro = { workspace = true, features = ["axum"] }
leptos_meta = { workspace = true, features = ["ssr"] }
leptos_router = { workspace = true, features = ["ssr"] }

View File

@@ -54,7 +54,7 @@ use leptos_meta::{generate_head_metadata_separated, MetaContext};
use leptos_router::*;
use once_cell::sync::OnceCell;
use parking_lot::RwLock;
use server_fn::redirect::REDIRECT_HEADER;
use server_fn::{error::NoCustomError, redirect::REDIRECT_HEADER};
use std::{fmt::Debug, io, pin::Pin, sync::Arc, thread::available_parallelism};
use tokio_util::task::LocalPoolHandle;
use tracing::Instrument;
@@ -329,7 +329,15 @@ async fn handle_server_fns_inner(
_ = tx.send(res);
});
rx.await.unwrap()
rx.await.unwrap_or_else(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
ServerFnError::<NoCustomError>::ServerError(e.to_string())
.ser()
.unwrap_or_default(),
)
.into_response()
})
}
pub type PinnedHtmlStream =
@@ -1586,6 +1594,14 @@ where
where
IV: IntoView + 'static,
{
// S represents the router's finished state allowing us to provide
// it to the user's server functions.
let state = options.clone();
let cx_with_state = move || {
provide_context::<S>(state.clone());
additional_context();
};
let mut router = self;
// register router paths
@@ -1601,7 +1617,7 @@ where
path,
LeptosOptions::from_ref(options),
app_fn.clone(),
additional_context.clone(),
cx_with_state.clone(),
method,
static_mode,
)
@@ -1621,7 +1637,7 @@ where
SsrMode::OutOfOrder => {
let s = render_app_to_stream_with_context(
LeptosOptions::from_ref(options),
additional_context.clone(),
cx_with_state.clone(),
app_fn.clone(),
);
match method {
@@ -1635,7 +1651,7 @@ where
SsrMode::PartiallyBlocked => {
let s = render_app_to_stream_with_context_and_replace_blocks(
LeptosOptions::from_ref(options),
additional_context.clone(),
cx_with_state.clone(),
app_fn.clone(),
true
);
@@ -1650,7 +1666,7 @@ where
SsrMode::InOrder => {
let s = render_app_to_stream_in_order_with_context(
LeptosOptions::from_ref(options),
additional_context.clone(),
cx_with_state.clone(),
app_fn.clone(),
);
match method {
@@ -1664,7 +1680,7 @@ where
SsrMode::Async => {
let s = render_app_async_with_context(
LeptosOptions::from_ref(options),
additional_context.clone(),
cx_with_state.clone(),
app_fn.clone(),
);
match method {
@@ -1683,9 +1699,9 @@ where
// register server functions
for (path, method) in server_fn::axum::server_fn_paths() {
let additional_context = additional_context.clone();
let cx_with_state = cx_with_state.clone();
let handler = move |req: Request<Body>| async move {
handle_server_fns_with_context(additional_context, req).await
handle_server_fns_with_context(cx_with_state, req).await
};
router = router.route(
path,

View File

@@ -206,3 +206,5 @@ fn ssr_option() {
runtime.dispose();
}
// TODO: remove simulated change

View File

@@ -276,6 +276,14 @@ impl ElementDescriptor for Custom {
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
/// Represents an HTML element.
///### Beginner's tip:
/// `HtmlElement<El>` implements [`std::ops::Deref`] where `El` implements [`std::ops::Deref`].
/// When `El` has a corresponding [`web_sys::HtmlElement`] -> `El` will implement [`std::ops::Deref`] for it.
/// For instance [`leptos::HtmlElement<Div>`] implements [`std::ops::Deref`] for [`web_sys::HtmlDivElement`]
/// Because of [Deref Coercion](https://doc.rust-lang.org/std/ops/trait.Deref.html#deref-coercion) you can call applicable [`web_sys::HtmlElement`] methods on `HtmlElement<El>` as if it were that type.
/// If both `HtmlElement<El>` and one of its derefs have a method with the same name, the dot syntax will call the method on the inherent impl (i.e. `HtmlElement<El>` or it's [`std::ops::Deref`] Target).
/// You may need to manually deref to access other methods, for example, `(*el).style()` to get the `CssStyleDeclaration` instead of calling [`leptos::HtmlElement::style`].
#[must_use = "You are creating an HtmlElement<_> but not using it. An unused view can \
cause your view to be rendered as () unexpectedly, and it can \
also cause issues with client-side hydration."]

View File

@@ -11,13 +11,13 @@ dependencies = [
[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"]
args = ["+nightly-2024-01-29", "test", "--doc"]
cwd = "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"]
args = ["+nightly-2024-01-29", "doc"]
cwd = "example"
install_crate = false

View File

@@ -1,7 +1,7 @@
use crate::{attribute_value, Mode};
use convert_case::{Case::Snake, Casing};
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
use quote::{format_ident, quote, quote_spanned};
use quote::{quote, quote_spanned};
use rstml::node::{KeyedAttribute, Node, NodeElement, NodeName};
use syn::{spanned::Spanned, Expr, Expr::Tuple, ExprLit, ExprPath, Lit};
@@ -534,12 +534,12 @@ pub(crate) fn directive_call_from_attribute_node(
attr: &KeyedAttribute,
directive_name: &str,
) -> TokenStream {
let handler = format_ident!("{directive_name}", span = attr.key.span());
let handler = syn::Ident::new(directive_name, attr.key.span());
let param = if let Some(value) = attr.value() {
quote! { #value.into() }
quote! { ::std::convert::Into::into(#value) }
} else {
quote! { () }
quote! { ().into() }
};
quote! { .directive(#handler, #param) }

View File

@@ -125,7 +125,7 @@ use runtime::*;
pub use runtime::{
as_child_of_current_owner, batch, create_runtime, current_runtime,
on_cleanup, run_as_child, set_current_runtime,
spawn_local_with_current_owner, spawn_local_with_owner,
spawn_local_with_current_owner, spawn_local_with_owner, try_batch,
try_spawn_local_with_current_owner, try_spawn_local_with_owner,
try_with_owner, untrack, untrack_with_diagnostics, with_current_owner,
with_owner, Owner, RuntimeId, ScopedFuture,
@@ -143,7 +143,8 @@ pub use suspense::{GlobalSuspenseContext, SuspenseContext};
pub use trigger::*;
pub use watch::*;
pub(crate) fn console_warn(s: &str) {
#[doc(hidden)]
pub fn console_warn(s: &str) {
cfg_if::cfg_if! {
if #[cfg(all(target_arch = "wasm32", any(feature = "csr", feature = "hydrate")))] {
web_sys::console::warn_1(&wasm_bindgen::JsValue::from_str(s));

View File

@@ -385,7 +385,10 @@ where
// client
create_render_effect({
let r = Rc::clone(&r);
move |_| r.load(false, id)
move |_| {
source.track();
r.load(false, id)
}
});
Resource {

View File

@@ -281,7 +281,7 @@ impl Runtime {
let source_map = self.node_sources.borrow();
for effect in subs.borrow().iter() {
if let Some(effect_sources) = source_map.get(*effect) {
effect_sources.borrow_mut().remove(&node);
effect_sources.borrow_mut().swap_remove(&node);
}
}
}
@@ -308,7 +308,18 @@ impl Runtime {
let subs = self.node_subscribers.borrow();
for source in sources.borrow().iter() {
if let Some(source) = subs.get(*source) {
source.borrow_mut().remove(&node_id);
// Using `.shift_remove()` here guarantees that dependencies
// of a signal are always triggered in the same order.
// This is important for cases in which, for example, the first effect
// conditionally checks that the signal value is `Some(_)`, and the
// second one unwraps its value; if they maintain this order, then the check
// will always run first, and will cancel the unwrap if it is None. But if the
// order can be inverted (by using .swap_remove() here), the unwrap will
// run first on a subsequent run.
//
// Maintaining execution order is the intention of using an IndexSet here anyway,
// but using .swap_remove() would undermine that goal.
source.borrow_mut().shift_remove(&node_id);
}
}
}
@@ -1386,12 +1397,30 @@ impl Drop for SetObserverOnDrop {
///
/// # Panics
/// Panics if the runtime has already been disposed.
///
/// To avoid panicking under any circumstances, use [`try_batch`].
#[cfg_attr(
any(debug_assertions, features = "ssr"),
instrument(level = "trace", skip_all,)
)]
#[inline(always)]
pub fn batch<T>(f: impl FnOnce() -> T) -> T {
try_batch(f).expect(
"tried to run a batched update in a runtime that has been disposed",
)
}
/// Attempts to batch any reactive updates, preventing effects from running until the whole
/// function has run. This allows you to prevent rerunning effects if multiple
/// signal updates might cause the same effect to run.
///
/// Unlike [`batch`], this will not panic if the runtime has been disposed.
#[cfg_attr(
any(debug_assertions, features = "ssr"),
instrument(level = "trace", skip_all,)
)]
#[inline(always)]
pub fn try_batch<T>(f: impl FnOnce() -> T) -> Result<T, ReactiveSystemError> {
with_runtime(move |runtime| {
let batching = SetBatchingOnDrop(runtime.batching.get());
runtime.batching.set(true);
@@ -1404,7 +1433,6 @@ pub fn batch<T>(f: impl FnOnce() -> T) -> T {
runtime.run_effects();
val
})
.expect("tried to run a batched update in a runtime that has been disposed")
}
struct SetBatchingOnDrop(bool);

View File

@@ -1,7 +1,10 @@
//use crate::{ServerFn, ServerFnError};
#[cfg(debug_assertions)]
use leptos_reactive::console_warn;
use leptos_reactive::{
batch, create_rw_signal, is_suppressing_resource_load, signal_prelude::*,
spawn_local, store_value, use_context, ReadSignal, RwSignal, StoredValue,
create_rw_signal, is_suppressing_resource_load, signal_prelude::*,
spawn_local, store_value, try_batch, use_context, ReadSignal, RwSignal,
StoredValue,
};
use server_fn::{error::ServerFnUrlError, ServerFn, ServerFnError};
use std::{cell::Cell, future::Future, pin::Pin, rc::Rc};
@@ -93,8 +96,18 @@ where
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
#[track_caller]
pub fn dispatch(&self, input: I) {
self.0.with_value(|a| a.dispatch(input))
#[cfg(debug_assertions)]
let loc = std::panic::Location::caller();
self.0.with_value(|a| {
a.dispatch(
input,
#[cfg(debug_assertions)]
loc,
)
})
}
/// Create an [Action].
@@ -366,7 +379,11 @@ where
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub fn dispatch(&self, input: I) {
pub fn dispatch(
&self,
input: I,
#[cfg(debug_assertions)] loc: &'static std::panic::Location<'static>,
) {
if !is_suppressing_resource_load() {
let fut = (self.action_fn)(&input);
self.input.set(Some(input));
@@ -379,7 +396,7 @@ where
pending_dispatches.set(pending_dispatches.get().saturating_sub(1));
spawn_local(async move {
let new_value = fut.await;
batch(move || {
let res = try_batch(move || {
value.set(Some(new_value));
input.set(None);
version.update(|n| *n += 1);
@@ -389,6 +406,18 @@ where
pending.set(false);
}
});
if res.is_err() {
#[cfg(debug_assertions)]
console_warn(&format!(
"At {loc}, you are dispatching an action in a runtime \
that has already been disposed. This may be because \
you are calling `.dispatch()` in the body of a \
component, during initial server-side rendering. If \
that's the case, you should probably be using \
`create_resource` instead of `create_action`."
));
}
})
}
}

View File

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

View File

@@ -158,7 +158,7 @@ impl MetaTagsContext {
move || {
let head = document().head().unwrap_throw();
_ = head.remove_child(&el);
els.borrow_mut().remove(&id);
els.borrow_mut().swap_remove(&id);
}
});

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.6.2"
version = "0.6.5"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

@@ -421,7 +421,7 @@ fn current_window_origin() -> String {
#[component]
pub fn ActionForm<ServFn>(
/// The action from which to build the form. This should include a URL, which can be generated
/// by default using [`create_server_action`](l:eptos_server::create_server_action) or added
/// by default using [`create_server_action`](leptos_server::create_server_action) or added
/// manually using [`using_server_fn`](leptos_server::Action::using_server_fn).
action: Action<
ServFn,
@@ -593,21 +593,38 @@ where
action_form
}
fn form_from_event(ev: &SubmitEvent) -> Option<HtmlFormElement> {
let submitter = ev.unchecked_ref::<SubmitEvent>().submitter();
match &submitter {
fn form_data_from_event(
ev: &SubmitEvent,
) -> Result<FormData, FromFormDataError> {
let submitter = ev.submitter();
let mut submitter_name_value = None;
let opt_form = match &submitter {
Some(el) => {
if let Some(form) = el.dyn_ref::<HtmlFormElement>() {
Some(form.clone())
} else if el.is_instance_of::<HtmlInputElement>()
|| el.is_instance_of::<HtmlButtonElement>()
{
} else if let Some(input) = el.dyn_ref::<HtmlInputElement>() {
submitter_name_value = Some((input.name(), input.value()));
Some(ev.target().unwrap().unchecked_into())
} else if let Some(button) = el.dyn_ref::<HtmlButtonElement>() {
submitter_name_value = Some((button.name(), button.value()));
Some(ev.target().unwrap().unchecked_into())
} else {
None
}
}
None => ev.target().map(|form| form.unchecked_into()),
};
match opt_form.as_ref().map(FormData::new_with_form) {
None => Err(FromFormDataError::MissingForm(ev.clone().into())),
Some(Err(e)) => Err(FromFormDataError::FormData(e)),
Some(Ok(form_data)) => {
if let Some((name, value)) = submitter_name_value {
form_data
.append_with_str(&name, &value)
.map_err(FromFormDataError::FormData)?;
}
Ok(form_data)
}
}
}
@@ -755,10 +772,8 @@ where
tracing::instrument(level = "trace", skip_all,)
)]
fn from_event(ev: &Event) -> Result<Self, FromFormDataError> {
let form = form_from_event(ev.unchecked_ref())
.ok_or_else(|| FromFormDataError::MissingForm(ev.clone()))?;
let form_data = FormData::new_with_form(&form)
.map_err(FromFormDataError::FormData)?;
let submit_ev = ev.unchecked_ref();
let form_data = form_data_from_event(submit_ev)?;
Self::from_form_data(&form_data)
.map_err(FromFormDataError::Deserialization)
}

View File

@@ -30,6 +30,8 @@ impl ParamsMap {
/// Inserts a value into the map.
#[inline(always)]
pub fn insert(&mut self, key: String, value: String) -> Option<String> {
use crate::history::url::unescape;
let value = unescape(&value);
self.0.insert(key, value)
}
@@ -76,12 +78,14 @@ impl Default for ParamsMap {
///
/// ```
/// # use leptos_router::params_map;
/// # #[cfg(feature = "ssr")] {
/// let map = params_map! {
/// "crate" => "leptos",
/// 42 => true, // where key & val: core::fmt::Display
/// };
/// assert_eq!(map.get("crate"), Some(&"leptos".to_string()));
/// assert_eq!(map.get("42"), Some(&true.to_string()))
/// # }
/// ```
// Original implementation included the below credits.
//

View File

@@ -15,6 +15,14 @@ pub struct Url {
pub hash: String,
}
#[cfg(feature = "ssr")]
pub fn unescape(s: &str) -> String {
percent_encoding::percent_decode_str(s)
.decode_utf8()
.unwrap()
.to_string()
}
#[cfg(not(feature = "ssr"))]
pub fn unescape(s: &str) -> String {
js_sys::decode_uri(s).unwrap().into()

View File

@@ -42,6 +42,21 @@ cfg_if! {
);
}
#[test]
fn create_matcher_should_build_params_collection_and_decode() {
let matcher = Matcher::new("/foo/:id");
let matched = matcher.test("/foo/%E2%89%A1abc%20123");
assert_eq!(
matched,
Some(PathMatch {
path: "/foo/%E2%89%A1abc%20123".into(),
params: params_map!(
"id" => "≡abc 123"
)
})
);
}
#[test]
fn create_matcher_should_match_past_end_when_ending_in_asterisk() {
let matcher = Matcher::new("/foo/bar/*");

View File

@@ -1,3 +1,4 @@
edition = "2021"
imports_granularity = "Crate"
max_width = 80
format_strings = true

View File

@@ -27,7 +27,7 @@ once_cell = "1"
actix-web = { version = "4", optional = true }
# axum
axum = { version = "0.7", optional = true, features = ["multipart"] }
axum = { version = "0.7", optional = true, default-features = false, features = ["multipart"] }
tower = { version = "0.4", optional = true }
tower-layer = { version = "0.3", optional = true }
@@ -75,7 +75,7 @@ url = "2"
default = ["json", "cbor"]
form-redirects = []
actix = ["ssr", "dep:actix-web", "dep:send_wrapper"]
axum = [
axum-no-default = [
"ssr",
"dep:axum",
"dep:hyper",
@@ -83,6 +83,10 @@ axum = [
"dep:tower",
"dep:tower-layer",
]
axum = [
"axum/default",
"axum-no-default",
]
browser = [
"dep:gloo-net",
"dep:js-sys",

View File

@@ -175,7 +175,6 @@ impl<E> ViaError<E> for WrapError<E> {
/// This means that other error types can easily be converted into it using the
/// `?` operator.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ServerFnError<E = NoCustomError> {
/// A user-defined custom error type, which defaults to [`NoCustomError`].
WrappedServerError(E),

View File

@@ -117,7 +117,7 @@ pub mod response;
#[cfg(feature = "actix")]
#[doc(hidden)]
pub use ::actix_web as actix_export;
#[cfg(feature = "axum")]
#[cfg(feature = "axum-no-default")]
#[doc(hidden)]
pub use ::axum as axum_export;
use client::Client;
@@ -456,7 +456,7 @@ impl<Req: 'static, Res: 'static> inventory::Collect
}
/// Axum integration.
#[cfg(feature = "axum")]
#[cfg(feature = "axum-no-default")]
pub mod axum {
use crate::{
middleware::{BoxedService, Service},

View File

@@ -26,7 +26,7 @@ pub trait Service<Request, Response> {
) -> Pin<Box<dyn Future<Output = Response> + Send>>;
}
#[cfg(feature = "axum")]
#[cfg(feature = "axum-no-default")]
mod axum {
use super::{BoxedService, Service};
use crate::{response::Res, ServerFnError};

View File

@@ -5,6 +5,7 @@ use futures::{Stream, StreamExt};
pub use gloo_net::http::Request;
use js_sys::{Reflect, Uint8Array};
use send_wrapper::SendWrapper;
use std::ops::{Deref, DerefMut};
use wasm_bindgen::JsValue;
use wasm_streams::ReadableStream;
use web_sys::{FormData, Headers, RequestInit, UrlSearchParams};
@@ -19,6 +20,32 @@ impl From<Request> for BrowserRequest {
}
}
impl From<BrowserRequest> for Request {
fn from(value: BrowserRequest) -> Self {
value.0.take()
}
}
impl From<BrowserRequest> for web_sys::Request {
fn from(value: BrowserRequest) -> Self {
value.0.take().into()
}
}
impl Deref for BrowserRequest {
type Target = Request;
fn deref(&self) -> &Self::Target {
self.0.deref()
}
}
impl DerefMut for BrowserRequest {
fn deref_mut(&mut self) -> &mut Self::Target {
self.0.deref_mut()
}
}
/// The `FormData` type available in the browser.
#[derive(Debug)]
pub struct BrowserFormData(pub(crate) SendWrapper<FormData>);

View File

@@ -7,7 +7,7 @@ use std::{borrow::Cow, future::Future};
#[cfg(feature = "actix")]
pub mod actix;
/// Request types for Axum.
#[cfg(feature = "axum")]
#[cfg(feature = "axum-no-default")]
pub mod axum;
/// Request types for the browser.
#[cfg(feature = "browser")]

View File

@@ -5,7 +5,7 @@ pub mod actix;
#[cfg(feature = "browser")]
pub mod browser;
/// Response types for Axum.
#[cfg(feature = "axum")]
#[cfg(feature = "axum-no-default")]
pub mod http;
/// Response types for [`reqwest`].
#[cfg(feature = "reqwest")]