mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 09:02:37 -05:00
Compare commits
2 Commits
resource-s
...
diagnostic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5ad8f5ae4 | ||
|
|
6b3e9cf85f |
28
Cargo.toml
28
Cargo.toml
@@ -26,22 +26,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.4.2"
|
||||
version = "0.4.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", version = "0.4.2" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.4.2" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.4.2" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.4.2" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.4.2" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.4.2" }
|
||||
server_fn = { path = "./server_fn", version = "0.4.2" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.4.2" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.4.2" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.4.2" }
|
||||
leptos_router = { path = "./router", version = "0.4.2" }
|
||||
leptos_meta = { path = "./meta", version = "0.4.2" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.4.2" }
|
||||
leptos = { path = "./leptos", version = "0.4.0" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.4.0" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.4.0" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.4.0" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.4.0" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.4.0" }
|
||||
server_fn = { path = "./server_fn", version = "0.4.0" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.4.0" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.4.0" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.4.0" }
|
||||
leptos_router = { path = "./router", version = "0.4.0" }
|
||||
leptos_meta = { path = "./meta", version = "0.4.0" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.4.0" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -97,15 +97,24 @@ args = ["+nightly", "doc"]
|
||||
cwd = "leptos_macro/example"
|
||||
install_crate = false
|
||||
|
||||
[tasks.ci-examples]
|
||||
[tasks.test-examples]
|
||||
description = "Run all unit and web tests for examples"
|
||||
cwd = "examples"
|
||||
command = "cargo"
|
||||
args = ["make", "ci-clean"]
|
||||
args = ["make", "test-unit-and-web"]
|
||||
|
||||
[tasks.verify-examples]
|
||||
description = "Run all quality checks and tests for examples"
|
||||
env = { CLEAN_AFTER_VERIFY = "true" }
|
||||
cwd = "examples"
|
||||
command = "cargo"
|
||||
args = ["make", "verify-flow"]
|
||||
|
||||
[tasks.clean-examples]
|
||||
description = "Clean all example projects"
|
||||
cwd = "examples"
|
||||
command = "cargo"
|
||||
args = ["make", "clean"]
|
||||
args = ["make", "clean-all"]
|
||||
|
||||
[env]
|
||||
RUSTFLAGS = ""
|
||||
|
||||
@@ -20,27 +20,18 @@ Create a basic Rust binary project
|
||||
cargo init leptos-tutorial
|
||||
```
|
||||
|
||||
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
|
||||
|
||||
```bash
|
||||
cargo add leptos --features=csr,nightly # or just csr if you're using stable Rust
|
||||
```
|
||||
|
||||
> Using `nightly` Rust, and the `nightly` feature in Leptos enables the function-call syntax for signal getters and setters that is used in most of this book.
|
||||
>
|
||||
> To use `nightly` Rust, you can run
|
||||
> We recommend using `nightly` Rust, as it enables [a few nice features](https://github.com/leptos-rs/leptos#nightly-note). To use `nightly` Rust with WebAssembly, you can run
|
||||
>
|
||||
> ```bash
|
||||
> rustup toolchain install nightly
|
||||
> rustup default nightly
|
||||
> rustup target add wasm32-unknown-unknown
|
||||
> ```
|
||||
>
|
||||
> If you’d rather use stable Rust with Leptos, you can do that too. In the guide and examples, you’ll just use the [`ReadSignal::get()`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html#impl-SignalGet%3CT%3E-for-ReadSignal%3CT%3E) and [`WriteSignal::set()`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html#impl-SignalGet%3CT%3E-for-ReadSignal%3CT%3E) methods instead of calling signal getters and setters as functions.
|
||||
|
||||
Make sure you've added the `wasm32-unknown-unknown` target do that Rust can compile your code to WebAssembly to run in the browser.
|
||||
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
|
||||
|
||||
```bash
|
||||
rustup target add wasm32-unknown-unknown
|
||||
cargo add leptos --features=csr,nightly # or just csr if you're using stable Rust
|
||||
```
|
||||
|
||||
Create a simple `index.html` in the root of the `leptos-tutorial` directory
|
||||
|
||||
1
docs/book/src/14_create_effect.md
Normal file
1
docs/book/src/14_create_effect.md
Normal file
@@ -0,0 +1 @@
|
||||
# Responding to Changes with create_effect
|
||||
@@ -23,6 +23,7 @@
|
||||
- [Transition](./async/12_transition.md)
|
||||
- [Actions](./async/13_actions.md)
|
||||
- [Interlude: Projecting Children](./interlude_projecting_children.md)
|
||||
- [Responding to Changes with `create_effect`](./14_create_effect.md)
|
||||
- [Global State Management](./15_global_state.md)
|
||||
- [Router](./router/README.md)
|
||||
- [Defining `<Routes/>`](./router/16_routes.md)
|
||||
|
||||
@@ -124,7 +124,7 @@ You can go even deeper. Say you want to have tabs for each contact’s address,
|
||||
|
||||
## `<Outlet/>`
|
||||
|
||||
Parent routes do not automatically render their nested routes. After all, they are just components; they don’t know exactly where they should render their children, and “just stick it at the end of the parent component” is not a great answer.
|
||||
Parent routes do not automatically render their nested routes. After all, they are just components; they don’t know exactly where they should render their children, and “just stick at at the end of the parent component” is not a great answer.
|
||||
|
||||
Instead, you tell a parent component where to render any nested components with an `<Outlet/>` component. The `<Outlet/>` simply renders one of two things:
|
||||
|
||||
|
||||
@@ -70,9 +70,9 @@ let id = move || {
|
||||
This can get a little messy: deriving a signal that wraps an `Option<_>` or `Result<_>` can involve a couple steps. But it’s worth doing this for two reasons:
|
||||
|
||||
1. It’s correct, i.e., it forces you to consider the cases, “What if the user doesn’t pass a value for this query field? What if they pass an invalid value?”
|
||||
2. It’s performant. Specifically, when you navigate between different paths that match the same `<Route/>` with only params or the query changing, you can get fine-grained updates to different parts of your app without rerendering. For example, navigating between different contacts in our contact-list example does a targeted update to the name field (and eventually contact info) without needing to replace or rerender the wrapping `<Contact/>`. This is what fine-grained reactivity is for.
|
||||
2. It’s performant. Specifically, when you navigate between different paths that match the same `<Route/>` with only params or the query changing, you can get fine-grained updates to different parts of your app without rerendering. For example, navigating between different contacts in our contact-list example does a targeted update to the name field (and eventually contact info) without needing to replacing or rerender the wrapping `<Contact/>`. This is what fine-grained reactivity is for.
|
||||
|
||||
> This is the same example from the previous section. The router is such an integrated system that it makes sense to provide a single example highlighting multiple features, even if we haven’t explained them all yet.
|
||||
> This is the same example from the previous section. The router is such an integrated system that it makes sense to provide a single example highlighting multiple features, even if we haven’t explain them all yet.
|
||||
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# The `<Form/>` Component
|
||||
|
||||
Links and forms sometimes seem completely unrelated. But, in fact, they work in very similar ways.
|
||||
Links and forms sometimes seem completely unrelated. But in fact, they work in very similar ways.
|
||||
|
||||
In plain HTML, there are three ways to navigate to another page:
|
||||
|
||||
1. An `<a>` element that links to another page: Navigates to the URL in its `href` attribute with the `GET` HTTP method.
|
||||
2. A `<form method="GET">`: Navigates to the URL in its `action` attribute with the `GET` HTTP method and the form data from its inputs encoded in the URL query string.
|
||||
3. A `<form method="POST">`: Navigates to the URL in its `action` attribute with the `POST` HTTP method and the form data from its inputs encoded in the body of the request.
|
||||
1. An `<a>` element that links to another page. Navigates to the URL in its `href` attribute with the `GET` HTTP method.
|
||||
2. A `<form method="GET">`. Navigates to the URL in its `action` attribute with the `GET` HTTP method and the form data from its inputs encoded in the URL query string.
|
||||
3. A `<form method="POST">`. Navigates to the URL in its `action` attribute with the `POST` HTTP method and the form data from its inputs encoded in the body of the request.
|
||||
|
||||
Since we have a client-side router, we can do client-side link navigations without reloading the page, i.e., without a full round-trip to the server and back. It makes sense that we can do client-side form navigations in the same way.
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@ let (value, set_value) = create_signal(cx, 0);
|
||||
|
||||
view! { cx,
|
||||
<Show
|
||||
when=move || { value() > 5 }
|
||||
when=move || value() > 5
|
||||
fallback=|cx| view! { cx, <Small/> }
|
||||
>
|
||||
<Big/>
|
||||
|
||||
@@ -19,7 +19,7 @@ fn NumericInput(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<label>
|
||||
"Type a number (or not!)"
|
||||
<input on:input=on_input/>
|
||||
<input type="number" on:input=on_input/>
|
||||
<p>
|
||||
"You entered "
|
||||
<strong>{value}</strong>
|
||||
@@ -69,7 +69,7 @@ fn NumericInput(cx: Scope) -> impl IntoView {
|
||||
<h1>"Error Handling"</h1>
|
||||
<label>
|
||||
"Type a number (or something that's not a number!)"
|
||||
<input on:input=on_input/>
|
||||
<input type="number" on:input=on_input/>
|
||||
<ErrorBoundary
|
||||
// the fallback receives a signal containing current errors
|
||||
fallback=|cx, errors| view! { cx,
|
||||
|
||||
@@ -117,7 +117,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
|
||||
|
||||
#[component]
|
||||
pub fn ButtonC(cx: Scope) -> impl IntoView {
|
||||
pub fn ButtonC<F>(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<button>"Toggle"</button>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
[tasks.integration-test]
|
||||
dependencies = ["cargo-leptos-e2e"]
|
||||
[tasks.test-e2e]
|
||||
dependencies = ["setup-node", "cargo-leptos-e2e"]
|
||||
|
||||
[tasks.cargo-leptos-e2e]
|
||||
command = "cargo"
|
||||
args = ["leptos", "end-to-end"]
|
||||
[tasks.clean-all]
|
||||
dependencies = ["clean-cargo", "clean-node_modules", "clean-playwright"]
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
[tasks.clean]
|
||||
dependencies = [
|
||||
"clean-cargo",
|
||||
"clean-trunk",
|
||||
"clean-node_modules",
|
||||
"clean-playwright",
|
||||
]
|
||||
|
||||
[tasks.clean-cargo]
|
||||
command = "cargo"
|
||||
args = ["clean"]
|
||||
|
||||
[tasks.clean-trunk]
|
||||
command = "trunk"
|
||||
args = ["clean"]
|
||||
|
||||
[tasks.clean-node_modules]
|
||||
script = '''
|
||||
project_dir=${PWD##*/}
|
||||
if [ "$project_dir" != "todomvc" ]; then
|
||||
find . -type d -name node_modules | xargs rm -rf
|
||||
fi
|
||||
'''
|
||||
|
||||
[tasks.clean-playwright]
|
||||
script = '''
|
||||
find . -name playwright-report -name playwright -name test-results | xargs rm -rf
|
||||
'''
|
||||
98
examples/cargo-make/common.toml
Normal file
98
examples/cargo-make/common.toml
Normal file
@@ -0,0 +1,98 @@
|
||||
[tasks.pre-clippy]
|
||||
env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features -- -D warnings" }
|
||||
|
||||
[tasks.check-style]
|
||||
description = "Check for style violations"
|
||||
dependencies = ["check-format-flow", "clippy-flow"]
|
||||
|
||||
[tasks.check-format]
|
||||
env = { LEPTOS_PROJECT_DIRECTORY = "../../" }
|
||||
args = ["fmt", "--", "--check", "--config-path", "${LEPTOS_PROJECT_DIRECTORY}"]
|
||||
|
||||
[tasks.clean-cargo]
|
||||
description = "Runs the cargo clean command."
|
||||
category = "Cleanup"
|
||||
command = "cargo"
|
||||
args = ["clean"]
|
||||
|
||||
[tasks.clean-trunk]
|
||||
description = "Runs the trunk clean command."
|
||||
category = "Cleanup"
|
||||
command = "trunk"
|
||||
args = ["clean"]
|
||||
|
||||
[tasks.clean-node_modules]
|
||||
description = "Delete all node_modules directories"
|
||||
category = "Cleanup"
|
||||
script = '''
|
||||
find . -type d -name node_modules | xargs rm -rf
|
||||
'''
|
||||
|
||||
[tasks.clean-playwright]
|
||||
description = "Delete playwright directories"
|
||||
category = "Cleanup"
|
||||
script = '''
|
||||
for pw_dir in $(find . -name playwright.config.ts | xargs dirname)
|
||||
do
|
||||
rm -rf $pw_dir/playwright-report pw_dir/playwright pw_dir/test-results
|
||||
done
|
||||
'''
|
||||
|
||||
[tasks.clean-all]
|
||||
description = "Delete all temporary directories"
|
||||
category = "Cleanup"
|
||||
dependencies = ["clean-cargo"]
|
||||
|
||||
[tasks.test-wasm]
|
||||
env = { CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome" }
|
||||
command = "cargo"
|
||||
args = ["make", "wasm-pack-test"]
|
||||
|
||||
[tasks.cargo-leptos-e2e]
|
||||
description = "Runs end to end tests with cargo leptos"
|
||||
command = "cargo"
|
||||
args = ["leptos", "end-to-end"]
|
||||
|
||||
[tasks.setup-node]
|
||||
description = "Install node dependencies and playwright browsers"
|
||||
env = { PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "1" }
|
||||
script = '''
|
||||
BOLD="\e[1m"
|
||||
GREEN="\e[0;32m"
|
||||
RED="\e[0;31m"
|
||||
RESET="\e[0m"
|
||||
|
||||
project_dir=$CARGO_MAKE_WORKING_DIRECTORY
|
||||
|
||||
# Discover commands
|
||||
if command -v pnpm; then
|
||||
NODE_CMD=pnpm
|
||||
PLAYWRIGHT_CMD=pnpm
|
||||
elif command -v npm; then
|
||||
NODE_CMD=npm
|
||||
PLAYWRIGHT_CMD=npx
|
||||
else
|
||||
echo "${RED}${BOLD}ERROR${RESET} - pnpm or npm is required by this task"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install node dependencies
|
||||
for node_path in $(find . -name package.json -not -path '*/node_modules/*')
|
||||
do
|
||||
node_dir=$(dirname $node_path)
|
||||
echo Install node dependencies for $node_dir
|
||||
cd $node_dir
|
||||
${NODE_CMD} install
|
||||
cd ${project_dir}
|
||||
done
|
||||
|
||||
# Install playwright browsers
|
||||
for pw_path in $(find . -name playwright.config.ts)
|
||||
do
|
||||
pw_dir=$(dirname $pw_path)
|
||||
echo Install playwright browsers for $pw_dir
|
||||
cd $pw_dir
|
||||
${PLAYWRIGHT_CMD} playwright install
|
||||
cd $project_dir
|
||||
done
|
||||
'''
|
||||
@@ -1,9 +0,0 @@
|
||||
[tasks.pre-clippy]
|
||||
env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features -- -D warnings" }
|
||||
|
||||
[tasks.check-style]
|
||||
dependencies = ["check-format-flow", "clippy-flow"]
|
||||
|
||||
[tasks.check-format]
|
||||
env = { LEPTOS_PROJECT_DIRECTORY = "../../" }
|
||||
args = ["fmt", "--", "--check", "--config-path", "${LEPTOS_PROJECT_DIRECTORY}"]
|
||||
@@ -1,32 +1,35 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/clean.toml" },
|
||||
{ path = "../cargo-make/lint.toml" },
|
||||
{ path = "../cargo-make/node.toml" },
|
||||
]
|
||||
|
||||
# CI Stages
|
||||
extend = [{ path = "../cargo-make/common.toml" }]
|
||||
|
||||
[tasks.ci]
|
||||
dependencies = ["prepare", "lint", "build", "test-flow", "integration-test"]
|
||||
|
||||
[tasks.ci-clean]
|
||||
dependencies = ["ci", "clean"]
|
||||
|
||||
[tasks.prepare]
|
||||
dependencies = ["setup-node"]
|
||||
|
||||
[tasks.lint]
|
||||
dependencies = ["check-style"]
|
||||
|
||||
[tasks.integration-test]
|
||||
|
||||
# ALIASES
|
||||
alias = "verify-flow"
|
||||
|
||||
[tasks.verify-flow]
|
||||
alias = "ci"
|
||||
description = "Provides pre and post hooks for verify"
|
||||
dependencies = ["pre-verify", "verify", "post-verify"]
|
||||
|
||||
[tasks.t]
|
||||
dependencies = ["test-flow"]
|
||||
[tasks.verify]
|
||||
description = "Run all quality checks and tests"
|
||||
dependencies = ["check-style", "test-unit-and-e2e"]
|
||||
|
||||
[tasks.it]
|
||||
alias = "integration-test"
|
||||
[tasks.test-unit-and-e2e]
|
||||
description = "Run all unit and e2e tests"
|
||||
dependencies = ["test-flow", "test-e2e-flow"]
|
||||
|
||||
[tasks.pre-verify]
|
||||
|
||||
[tasks.post-verify]
|
||||
dependencies = ["maybe-clean-all"]
|
||||
|
||||
[tasks.maybe-clean-all]
|
||||
description = "Used to clean up locally after call to verify-examples"
|
||||
condition = { env_true = ["CLEAN_AFTER_VERIFY"] }
|
||||
|
||||
[tasks.test-e2e-flow]
|
||||
description = "Provides pre and post hooks for test-e2e"
|
||||
dependencies = ["pre-test-e2e", "test-e2e", "post-test-e2e"]
|
||||
|
||||
[tasks.pre-test-e2e]
|
||||
|
||||
[tasks.test-e2e]
|
||||
|
||||
[tasks.post-test-e2e]
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
[tasks.setup-node]
|
||||
description = "Install node dependencies and playwright browsers"
|
||||
env = { PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "1" }
|
||||
script = '''
|
||||
BOLD="\e[1m"
|
||||
GREEN="\e[0;32m"
|
||||
RED="\e[0;31m"
|
||||
RESET="\e[0m"
|
||||
|
||||
project_dir=$CARGO_MAKE_WORKING_DIRECTORY
|
||||
|
||||
# Discover commands
|
||||
if command -v pnpm; then
|
||||
NODE_CMD=pnpm
|
||||
PLAYWRIGHT_CMD=pnpm
|
||||
elif command -v npm; then
|
||||
NODE_CMD=npm
|
||||
PLAYWRIGHT_CMD=npx
|
||||
else
|
||||
echo "${RED}${BOLD}ERROR${RESET} - pnpm or npm is required by this task"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install node dependencies
|
||||
for node_path in $(find . -name package.json -not -path '*/node_modules/*')
|
||||
do
|
||||
node_dir=$(dirname $node_path)
|
||||
echo Install node dependencies for $node_dir
|
||||
cd $node_dir
|
||||
${NODE_CMD} install
|
||||
cd ${project_dir}
|
||||
done
|
||||
|
||||
# Install playwright browsers
|
||||
for pw_path in $(find . -name playwright.config.ts)
|
||||
do
|
||||
pw_dir=$(dirname $pw_path)
|
||||
echo Install playwright browsers for $pw_dir
|
||||
cd $pw_dir
|
||||
${PLAYWRIGHT_CMD} playwright install
|
||||
cd $project_dir
|
||||
done
|
||||
'''
|
||||
@@ -1,4 +1,7 @@
|
||||
extend = [{ path = "../cargo-make/playwright.toml" }]
|
||||
|
||||
[tasks.integration-test]
|
||||
dependencies = ["test-playwright-autostart"]
|
||||
[tasks.test-e2e]
|
||||
dependencies = ["setup-node", "test-playwright-autostart"]
|
||||
|
||||
[tasks.clean-all]
|
||||
dependencies = ["clean-cargo", "clean-node_modules", "clean-playwright"]
|
||||
|
||||
@@ -1,8 +1,22 @@
|
||||
[tasks.clean-playwright]
|
||||
description = "Delete playwright directories"
|
||||
category = "Cleanup"
|
||||
script = '''
|
||||
for pw_dir in $(find . -name playwright.config.ts | xargs dirname)
|
||||
do
|
||||
rm -rf $pw_dir/playwright-report pw_dir/playwright pw_dir/test-results
|
||||
done
|
||||
'''
|
||||
|
||||
[tasks.test-playwright-autostart]
|
||||
description = "Run playwright test with server autostart"
|
||||
category = "Test"
|
||||
command = "npm"
|
||||
args = ["run", "e2e:auto-start"]
|
||||
|
||||
[tasks.test-playwright]
|
||||
description = "Run playwright test"
|
||||
category = "Test"
|
||||
script = '''
|
||||
BOLD="\e[1m"
|
||||
GREEN="\e[0;32m"
|
||||
@@ -32,6 +46,8 @@ done
|
||||
'''
|
||||
|
||||
[tasks.test-playwright-ui]
|
||||
description = "Run playwright test --ui"
|
||||
category = "Test"
|
||||
script = '''
|
||||
BOLD="\e[1m"
|
||||
GREEN="\e[0;32m"
|
||||
@@ -61,6 +77,8 @@ done
|
||||
'''
|
||||
|
||||
[tasks.test-playwright-report]
|
||||
description = "Run playwright show-report"
|
||||
category = "Test"
|
||||
script = '''
|
||||
BOLD="\e[1m"
|
||||
GREEN="\e[0;32m"
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
command = "trunk"
|
||||
args = ["build"]
|
||||
|
||||
[tasks.clean-trunk]
|
||||
command = "trunk"
|
||||
args = ["clean"]
|
||||
|
||||
[tasks.start-trunk]
|
||||
command = "trunk"
|
||||
args = ["serve", "${@}"]
|
||||
args = ["serve", "--open"]
|
||||
|
||||
[tasks.stop-trunk]
|
||||
script = '''
|
||||
|
||||
@@ -5,7 +5,5 @@ condition = { env_true = ["RUN_CARGO_TEST"] }
|
||||
[tasks.post-test]
|
||||
dependencies = ["test-wasm"]
|
||||
|
||||
[tasks.test-wasm]
|
||||
env = { CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome" }
|
||||
command = "cargo"
|
||||
args = ["make", "wasm-pack-test"]
|
||||
[tasks.clean-all]
|
||||
dependencies = ["clean-cargo", "clean-trunk"]
|
||||
|
||||
@@ -25,7 +25,7 @@ leptos_meta = { path = "../../meta" }
|
||||
leptos_router = { path = "../../router" }
|
||||
log = "0.4"
|
||||
gloo-net = { git = "https://github.com/rustwasm/gloo" }
|
||||
wasm-bindgen = "=0.2.87"
|
||||
wasm-bindgen = "=0.2.86"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -67,10 +67,24 @@ pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=Counter/>
|
||||
<Route path="form" view=FormCounter/>
|
||||
<Route path="multi" view=MultiuserCounter/>
|
||||
<Route path="multi" view=NotFound/>
|
||||
<Route
|
||||
path=""
|
||||
view=|cx| {
|
||||
view! { cx, <Counter/> }
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="form"
|
||||
view=|cx| {
|
||||
view! { cx, <FormCounter/> }
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="multi"
|
||||
view=|cx| {
|
||||
view! { cx, <MultiuserCounter/> }
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
@@ -161,9 +175,13 @@ pub fn FormCounter(cx: Scope) -> impl IntoView {
|
||||
"This counter uses forms to set the value on the server. When progressively enhanced, it should behave identically to the “Simple Counter.”"
|
||||
</p>
|
||||
<div>
|
||||
// calling a server function is the same as POSTing to its API URL
|
||||
// so we can just do that with a form and button
|
||||
<ActionForm action=clear>
|
||||
<input type="submit" value="Clear"/>
|
||||
</ActionForm>
|
||||
// We can submit named arguments to the server functions
|
||||
// by including them as input values with the same name
|
||||
<ActionForm action=adjust>
|
||||
<input type="hidden" name="delta" value="-1"/>
|
||||
<input type="hidden" name="msg" value="form value down"/>
|
||||
@@ -238,14 +256,3 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn NotFound(cx: Scope) -> impl IntoView {
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
let resp = expect_context::<leptos_actix::ResponseOptions>(cx);
|
||||
resp.set_status(actix_web::http::StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
view! { cx, <h1>"Not Found"</h1> }
|
||||
}
|
||||
|
||||
@@ -52,36 +52,15 @@ cfg_if! {
|
||||
App::new()
|
||||
.service(counter_events)
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
// serve JS/WASM/CSS from `pkg`
|
||||
.service(Files::new("/pkg", format!("{site_root}/pkg")))
|
||||
// serve other assets from the `assets` directory
|
||||
.service(Files::new("/assets", site_root))
|
||||
// serve the favicon from /favicon.ico
|
||||
.service(favicon)
|
||||
.leptos_routes(
|
||||
leptos_options.to_owned(),
|
||||
routes.to_owned(),
|
||||
Counters,
|
||||
)
|
||||
.app_data(web::Data::new(leptos_options.to_owned()))
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), |cx| view! { cx, <Counters/> })
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
#[actix_web::get("favicon.ico")]
|
||||
async fn favicon(
|
||||
leptos_options: actix_web::web::Data<leptos::LeptosOptions>,
|
||||
) -> actix_web::Result<actix_files::NamedFile> {
|
||||
let leptos_options = leptos_options.into_inner();
|
||||
let site_root = &leptos_options.site_root;
|
||||
Ok(actix_files::NamedFile::open(format!(
|
||||
"{site_root}/favicon.ico"
|
||||
))?)
|
||||
}
|
||||
}
|
||||
|
||||
// client-only main for Trunk
|
||||
else {
|
||||
|
||||
@@ -5,23 +5,10 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["csr"] }
|
||||
leptos_meta = { path = "../../meta", features = ["csr"] }
|
||||
log = "0.4"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen = "0.2.87"
|
||||
wasm-bindgen-test = "0.3.37"
|
||||
pretty_assertions = "1.3.0"
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
|
||||
[dev-dependencies.web-sys]
|
||||
features = [
|
||||
"Event",
|
||||
"EventInit",
|
||||
"EventTarget",
|
||||
"HtmlElement",
|
||||
"HtmlInputElement",
|
||||
"XPathResult",
|
||||
]
|
||||
version = "0.3.64"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/wasm-test.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
{ path = "../cargo-make/playwright-test.toml" },
|
||||
]
|
||||
|
||||
@@ -14,6 +14,7 @@ test.describe("Add 1000 Counters", () => {
|
||||
await ui.addOneThousandCounters();
|
||||
await ui.addOneThousandCounters();
|
||||
|
||||
await expect(ui.total).toHaveText("0");
|
||||
await expect(ui.counters).toHaveText("3000");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ test.describe("Add Counter", () => {
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
|
||||
await expect(ui.total).toHaveText("0");
|
||||
await expect(ui.counters).toHaveText("3");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,5 +12,6 @@ test.describe("Decrement Count", () => {
|
||||
await ui.decrementCount();
|
||||
|
||||
await expect(ui.total).toHaveText("-3");
|
||||
await expect(ui.counters).toHaveText("1");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,5 +26,6 @@ test.describe("Enter Count", () => {
|
||||
await ui.enterCount("50", 1);
|
||||
|
||||
await expect(ui.total).toHaveText("250");
|
||||
await expect(ui.counters).toHaveText("3");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,5 +12,6 @@ test.describe("Increment Count", () => {
|
||||
await ui.incrementCount();
|
||||
|
||||
await expect(ui.total).toHaveText("3");
|
||||
await expect(ui.counters).toHaveText("1");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ test.describe("Remove Counter", () => {
|
||||
|
||||
await ui.removeCounter(1);
|
||||
|
||||
await expect(ui.total).toHaveText("0");
|
||||
await expect(ui.counters).toHaveText("2");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
|
||||
const MANY_COUNTERS: usize = 1000;
|
||||
|
||||
type CounterHolder = Vec<(usize, (ReadSignal<i32>, WriteSignal<i32>))>;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct CounterUpdater {
|
||||
set_counters: WriteSignal<CounterHolder>,
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
let (next_counter_id, set_next_counter_id) = create_signal(cx, 0);
|
||||
let (counters, set_counters) = create_signal::<CounterHolder>(cx, vec![]);
|
||||
provide_context(cx, CounterUpdater { set_counters });
|
||||
provide_meta_context(cx);
|
||||
|
||||
let add_counter = move |_| {
|
||||
let id = next_counter_id.get();
|
||||
let sig = create_signal(cx, 0);
|
||||
set_counters.update(move |counters| counters.push((id, sig)));
|
||||
set_next_counter_id.update(|id| *id += 1);
|
||||
};
|
||||
|
||||
let add_many_counters = move |_| {
|
||||
let next_id = next_counter_id.get();
|
||||
let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
|
||||
let signal = create_signal(cx, 0);
|
||||
(id, signal)
|
||||
});
|
||||
|
||||
set_counters.update(move |counters| counters.extend(new_counters));
|
||||
set_next_counter_id.update(|id| *id += MANY_COUNTERS);
|
||||
};
|
||||
|
||||
let clear_counters = move |_| {
|
||||
set_counters.update(|counters| counters.clear());
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<Title text="Counters (Stable)" />
|
||||
<div>
|
||||
<button on:click=add_counter>
|
||||
"Add Counter"
|
||||
</button>
|
||||
<button on:click=add_many_counters>
|
||||
{format!("Add {MANY_COUNTERS} Counters")}
|
||||
</button>
|
||||
<button on:click=clear_counters>
|
||||
"Clear Counters"
|
||||
</button>
|
||||
<p>
|
||||
"Total: "
|
||||
<span data-testid="total">{move ||
|
||||
counters.get()
|
||||
.iter()
|
||||
.map(|(_, (count, _))| count.get())
|
||||
.sum::<i32>()
|
||||
.to_string()
|
||||
}</span>
|
||||
" from "
|
||||
<span data-testid="counters">{move || counters.with(|counters| counters.len()).to_string()}</span>
|
||||
" counters."
|
||||
</p>
|
||||
<ul>
|
||||
<For
|
||||
each={move || counters.get()}
|
||||
key={|counter| counter.0}
|
||||
view=move |cx, (id, (value, set_value))| {
|
||||
view! {
|
||||
cx,
|
||||
<Counter id value set_value/>
|
||||
}
|
||||
}
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Counter(
|
||||
cx: Scope,
|
||||
id: usize,
|
||||
value: ReadSignal<i32>,
|
||||
set_value: WriteSignal<i32>,
|
||||
) -> impl IntoView {
|
||||
let CounterUpdater { set_counters } = use_context(cx).unwrap();
|
||||
|
||||
let input = move |ev| {
|
||||
set_value
|
||||
.set(event_target_value(&ev).parse::<i32>().unwrap_or_default())
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<li>
|
||||
<button data-testid="decrement_count" on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
|
||||
<input data-testid="counter_input" type="text"
|
||||
prop:value={move || value.get().to_string()}
|
||||
on:input=input
|
||||
/>
|
||||
<span>{value}</span>
|
||||
<button data-testid="increment_count" on:click=move |_| set_value.update(move |value| *value += 1)>"+1"</button>
|
||||
<button data-testid="remove_counter" on:click=move |_| set_counters.update(move |counters| counters.retain(|(counter_id, _)| counter_id != &id))>"x"</button>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
use counters_stable::Counters;
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
@@ -6,3 +5,108 @@ fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| view! { cx, <Counters/> })
|
||||
}
|
||||
|
||||
const MANY_COUNTERS: usize = 1000;
|
||||
|
||||
type CounterHolder = Vec<(usize, (ReadSignal<i32>, WriteSignal<i32>))>;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct CounterUpdater {
|
||||
set_counters: WriteSignal<CounterHolder>,
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
let (next_counter_id, set_next_counter_id) = create_signal(cx, 0);
|
||||
let (counters, set_counters) = create_signal::<CounterHolder>(cx, vec![]);
|
||||
provide_context(cx, CounterUpdater { set_counters });
|
||||
|
||||
let add_counter = move |_| {
|
||||
let id = next_counter_id.get();
|
||||
let sig = create_signal(cx, 0);
|
||||
set_counters.update(move |counters| counters.push((id, sig)));
|
||||
set_next_counter_id.update(|id| *id += 1);
|
||||
};
|
||||
|
||||
let add_many_counters = move |_| {
|
||||
let next_id = next_counter_id.get();
|
||||
let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
|
||||
let signal = create_signal(cx, 0);
|
||||
(id, signal)
|
||||
});
|
||||
|
||||
set_counters.update(move |counters| counters.extend(new_counters));
|
||||
set_next_counter_id.update(|id| *id += MANY_COUNTERS);
|
||||
};
|
||||
|
||||
let clear_counters = move |_| {
|
||||
set_counters.update(|counters| counters.clear());
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<div>
|
||||
<button on:click=add_counter>
|
||||
"Add Counter"
|
||||
</button>
|
||||
<button on:click=add_many_counters>
|
||||
{format!("Add {MANY_COUNTERS} Counters")}
|
||||
</button>
|
||||
<button on:click=clear_counters>
|
||||
"Clear Counters"
|
||||
</button>
|
||||
<p>
|
||||
"Total: "
|
||||
<span data-testid="total">{move ||
|
||||
counters.get()
|
||||
.iter()
|
||||
.map(|(_, (count, _))| count.get())
|
||||
.sum::<i32>()
|
||||
.to_string()
|
||||
}</span>
|
||||
" from "
|
||||
<span data-testid="counters">{move || counters.with(|counters| counters.len()).to_string()}</span>
|
||||
" counters."
|
||||
</p>
|
||||
<ul>
|
||||
<For
|
||||
each={move || counters.get()}
|
||||
key={|counter| counter.0}
|
||||
view=move |cx, (id, (value, set_value))| {
|
||||
view! {
|
||||
cx,
|
||||
<Counter id value set_value/>
|
||||
}
|
||||
}
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Counter(
|
||||
cx: Scope,
|
||||
id: usize,
|
||||
value: ReadSignal<i32>,
|
||||
set_value: WriteSignal<i32>,
|
||||
) -> impl IntoView {
|
||||
let CounterUpdater { set_counters } = use_context(cx).unwrap();
|
||||
|
||||
let input = move |ev| {
|
||||
set_value
|
||||
.set(event_target_value(&ev).parse::<i32>().unwrap_or_default())
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<li>
|
||||
<button id="decrement_count" on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
|
||||
<input type="text"
|
||||
prop:value={move || value.get().to_string()}
|
||||
on:input=input
|
||||
/>
|
||||
<span>{value}</span>
|
||||
<button id="increment_count" on:click=move |_| set_value.update(move |value| *value += 1)>"+1"</button>
|
||||
<button on:click=move |_| set_counters.update(move |counters| counters.retain(|(counter_id, _)| counter_id != &id))>"x"</button>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_increase_the_number_of_counters() {
|
||||
// Given
|
||||
ui::view_counters();
|
||||
|
||||
// When
|
||||
ui::add_1k_counters();
|
||||
ui::add_1k_counters();
|
||||
ui::add_1k_counters();
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::counters(), 3000);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_increase_the_number_of_counters() {
|
||||
// Given
|
||||
ui::view_counters();
|
||||
|
||||
// When
|
||||
ui::add_counter();
|
||||
ui::add_counter();
|
||||
ui::add_counter();
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::counters(), 3);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_reset_the_counts() {
|
||||
// Given
|
||||
ui::view_counters();
|
||||
ui::add_counter();
|
||||
ui::add_counter();
|
||||
ui::add_counter();
|
||||
|
||||
// When
|
||||
ui::clear_counters();
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::total(), 0);
|
||||
assert_eq!(ui::counters(), 0);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_decrease_the_total_count() {
|
||||
// Given
|
||||
ui::view_counters();
|
||||
ui::add_counter();
|
||||
|
||||
// When
|
||||
ui::decrement_counter(1);
|
||||
ui::decrement_counter(1);
|
||||
ui::decrement_counter(1);
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::total(), -3);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_increase_the_total_count() {
|
||||
// Given
|
||||
ui::view_counters();
|
||||
ui::add_counter();
|
||||
|
||||
// When
|
||||
ui::enter_count(1, 5);
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::total(), 5);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_decrease_the_total_count() {
|
||||
// Given
|
||||
ui::view_counters();
|
||||
ui::add_counter();
|
||||
ui::add_counter();
|
||||
ui::add_counter();
|
||||
|
||||
// When
|
||||
ui::enter_count(1, 100);
|
||||
ui::enter_count(2, 100);
|
||||
ui::enter_count(3, 100);
|
||||
ui::enter_count(1, 50);
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::total(), 250);
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
use counters_stable::Counters;
|
||||
use leptos::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::{Element, Event, EventInit, HtmlElement, HtmlInputElement};
|
||||
|
||||
// Actions
|
||||
|
||||
pub fn add_1k_counters() {
|
||||
find_by_text("Add 1000 Counters").click();
|
||||
}
|
||||
|
||||
pub fn add_counter() {
|
||||
find_by_text("Add Counter").click();
|
||||
}
|
||||
|
||||
pub fn clear_counters() {
|
||||
find_by_text("Clear Counters").click();
|
||||
}
|
||||
|
||||
pub fn decrement_counter(index: u32) {
|
||||
counter_html_element(index, "decrement_count").click();
|
||||
}
|
||||
|
||||
pub fn enter_count(index: u32, count: i32) {
|
||||
let input = counter_input_element(index, "counter_input");
|
||||
input.set_value(count.to_string().as_str());
|
||||
let mut event_init = EventInit::new();
|
||||
event_init.bubbles(true);
|
||||
let event = Event::new_with_event_init_dict("input", &event_init).unwrap();
|
||||
input.dispatch_event(&event).unwrap();
|
||||
}
|
||||
|
||||
pub fn increment_counter(index: u32) {
|
||||
counter_html_element(index, "increment_count").click();
|
||||
}
|
||||
|
||||
pub fn remove_counter(index: u32) {
|
||||
counter_html_element(index, "remove_counter").click();
|
||||
}
|
||||
|
||||
pub fn view_counters() {
|
||||
remove_existing_counters();
|
||||
mount_to_body(|cx| view! { cx, <Counters/> });
|
||||
}
|
||||
|
||||
// Results
|
||||
|
||||
pub fn counters() -> i32 {
|
||||
data_test_id("counters").parse::<i32>().unwrap()
|
||||
}
|
||||
|
||||
pub fn title() -> String {
|
||||
leptos::document().title()
|
||||
}
|
||||
|
||||
pub fn total() -> i32 {
|
||||
data_test_id("total").parse::<i32>().unwrap()
|
||||
}
|
||||
|
||||
// Internal
|
||||
|
||||
fn counter_element(index: u32, text: &str) -> Element {
|
||||
let selector =
|
||||
format!("li:nth-child({}) [data-testid=\"{}\"]", index, text);
|
||||
leptos::document()
|
||||
.query_selector(&selector)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn counter_html_element(index: u32, text: &str) -> HtmlElement {
|
||||
counter_element(index, text)
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn counter_input_element(index: u32, text: &str) -> HtmlInputElement {
|
||||
counter_element(index, text)
|
||||
.dyn_into::<HtmlInputElement>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn data_test_id(id: &str) -> String {
|
||||
let selector = format!("[data-testid=\"{}\"]", id);
|
||||
leptos::document()
|
||||
.query_selector(&selector)
|
||||
.unwrap()
|
||||
.expect("counters not found")
|
||||
.text_content()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn find_by_text(text: &str) -> HtmlElement {
|
||||
let xpath = format!("//*[text()='{}']", text);
|
||||
let document = leptos::document();
|
||||
document
|
||||
.evaluate(&xpath, &document)
|
||||
.unwrap()
|
||||
.iterate_next()
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn remove_existing_counters() {
|
||||
if let Some(counter) =
|
||||
leptos::document().query_selector("body div").unwrap()
|
||||
{
|
||||
counter.remove();
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod counters_page;
|
||||
@@ -1,18 +0,0 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_increase_the_total_count() {
|
||||
// Given
|
||||
ui::view_counters();
|
||||
ui::add_counter();
|
||||
|
||||
// When
|
||||
ui::increment_counter(1);
|
||||
ui::increment_counter(1);
|
||||
ui::increment_counter(1);
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::total(), 3);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
// Test Suites
|
||||
pub mod add_1k_counters;
|
||||
pub mod add_counter;
|
||||
pub mod clear_counters;
|
||||
pub mod decrement_counter;
|
||||
pub mod enter_count;
|
||||
pub mod increment_counter;
|
||||
pub mod remove_counter;
|
||||
pub mod view_counters;
|
||||
|
||||
pub mod fixtures;
|
||||
pub use fixtures::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
@@ -1,18 +0,0 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_decrement_the_number_of_counters() {
|
||||
// Given
|
||||
ui::view_counters();
|
||||
ui::add_counter();
|
||||
ui::add_counter();
|
||||
ui::add_counter();
|
||||
|
||||
// When
|
||||
ui::remove_counter(2);
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::counters(), 2);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_see_the_initial_counts() {
|
||||
// When
|
||||
ui::view_counters();
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::total(), 0);
|
||||
assert_eq!(ui::counters(), 0);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_see_the_title() {
|
||||
// When
|
||||
ui::view_counters();
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::title(), "Counters (Stable)");
|
||||
}
|
||||
@@ -11,7 +11,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
<h1>"Error Handling"</h1>
|
||||
<label>
|
||||
"Type a number (or something that's not a number!)"
|
||||
<input on:input=on_input/>
|
||||
<input type="number" on:input=on_input/>
|
||||
// If an `Err(_) had been rendered inside the <ErrorBoundary/>,
|
||||
// the fallback will be displayed. Otherwise, the children of the
|
||||
// <ErrorBoundary/> will be displayed.
|
||||
|
||||
@@ -14,6 +14,10 @@ cfg_if! {
|
||||
async fn css() -> impl Responder {
|
||||
actix_files::NamedFile::open_async("./style.css").await
|
||||
}
|
||||
#[get("/favicon.ico")]
|
||||
async fn favicon() -> impl Responder {
|
||||
actix_files::NamedFile::open_async("./target/site//favicon.ico").await
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
@@ -29,34 +33,17 @@ cfg_if! {
|
||||
let site_root = &leptos_options.site_root;
|
||||
|
||||
App::new()
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.service(Files::new("/pkg", format!("{site_root}/pkg")))
|
||||
.service(Files::new("/assets", site_root))
|
||||
.service(favicon)
|
||||
.service(css)
|
||||
.leptos_routes(
|
||||
leptos_options.to_owned(),
|
||||
routes.to_owned(),
|
||||
|cx| view! { cx, <App/> },
|
||||
)
|
||||
.app_data(web::Data::new(leptos_options.to_owned()))
|
||||
.service(favicon)
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), |cx| view! { cx, <App/> })
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
#[actix_web::get("favicon.ico")]
|
||||
async fn favicon(
|
||||
leptos_options: actix_web::web::Data<leptos::LeptosOptions>,
|
||||
) -> actix_web::Result<actix_files::NamedFile> {
|
||||
let leptos_options = leptos_options.into_inner();
|
||||
let site_root = &leptos_options.site_root;
|
||||
Ok(actix_files::NamedFile::open(format!(
|
||||
"{site_root}/favicon.ico"
|
||||
))?)
|
||||
}
|
||||
} else {
|
||||
fn main() {
|
||||
use hackernews::{App};
|
||||
|
||||
@@ -27,11 +27,6 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
view=Post
|
||||
ssr=SsrMode::Async
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/*any"
|
||||
view=NotFound
|
||||
/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
@@ -187,14 +182,3 @@ pub async fn get_post(id: usize) -> Result<Option<Post>, ServerFnError> {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
Ok(POSTS.iter().find(|post| post.id == id).cloned())
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn NotFound(cx: Scope) -> impl IntoView {
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
let resp = expect_context::<leptos_actix::ResponseOptions>(cx);
|
||||
resp.set_status(actix_web::http::StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
view! { cx, <h1>"Not Found"</h1> }
|
||||
}
|
||||
|
||||
@@ -24,11 +24,12 @@ async fn main() -> std::io::Result<()> {
|
||||
|
||||
App::new()
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.service(Files::new("/pkg", format!("{site_root}/pkg")))
|
||||
.service(Files::new("/assets", site_root))
|
||||
.service(favicon)
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), App)
|
||||
.app_data(web::Data::new(leptos_options.to_owned()))
|
||||
.leptos_routes(
|
||||
leptos_options.to_owned(),
|
||||
routes.to_owned(),
|
||||
|cx| view! { cx, <App/> },
|
||||
)
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
@@ -36,18 +37,6 @@ async fn main() -> std::io::Result<()> {
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[actix_web::get("favicon.ico")]
|
||||
async fn favicon(
|
||||
leptos_options: actix_web::web::Data<leptos::LeptosOptions>,
|
||||
) -> actix_web::Result<actix_files::NamedFile> {
|
||||
let leptos_options = leptos_options.into_inner();
|
||||
let site_root = &leptos_options.site_root;
|
||||
Ok(actix_files::NamedFile::open(format!(
|
||||
"{site_root}/favicon.ico"
|
||||
))?)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn main() {
|
||||
// no client-side main function
|
||||
|
||||
@@ -30,25 +30,6 @@ npm install -D tailwindcss
|
||||
|
||||
If you'd rather not use `npm`, you can install the Tailwind binary [here](https://github.com/tailwindlabs/tailwindcss/releases).
|
||||
|
||||
## Adding Tailwind plugins
|
||||
|
||||
If you'd like to add [Tailwind plugins](https://tailwindcss.com/docs/plugins), such as [DaisyUI](https://daisyui.com/), you can do the following:
|
||||
|
||||
`npm install -D daisyui@latest`
|
||||
|
||||
Then add the plugin to your exports in `tailwind.config.js` :
|
||||
|
||||
```javascript
|
||||
module.exports = {
|
||||
//...
|
||||
plugins: [require("daisyui")],
|
||||
};
|
||||
```
|
||||
|
||||
And re-run the following to generate the css:
|
||||
|
||||
`npx tailwindcss -i ./input.css -o ./style/output.css --watch`
|
||||
|
||||
## Setting up with VS Code and Additional Tools
|
||||
|
||||
If you're using VS Code, add the following to your `settings.json`
|
||||
|
||||
@@ -47,32 +47,14 @@ cfg_if! {
|
||||
App::new()
|
||||
.service(css)
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.service(Files::new("/pkg", format!("{site_root}/pkg")))
|
||||
.service(Files::new("/assets", site_root))
|
||||
.service(favicon)
|
||||
.leptos_routes(
|
||||
leptos_options.to_owned(),
|
||||
routes.to_owned(),
|
||||
TodoApp,
|
||||
)
|
||||
.app_data(web::Data::new(leptos_options.to_owned()))
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), |cx| view! { cx, <TodoApp/> })
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
#[actix_web::get("favicon.ico")]
|
||||
async fn favicon(
|
||||
leptos_options: actix_web::web::Data<leptos::LeptosOptions>,
|
||||
) -> actix_web::Result<actix_files::NamedFile> {
|
||||
let leptos_options = leptos_options.into_inner();
|
||||
let site_root = &leptos_options.site_root;
|
||||
Ok(actix_files::NamedFile::open(format!(
|
||||
"{site_root}/favicon.ico"
|
||||
))?)
|
||||
}
|
||||
} else {
|
||||
fn main() {
|
||||
// no client-side main function
|
||||
|
||||
@@ -92,8 +92,10 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
</header>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=Todos/>
|
||||
<Route path="/*any" view=NotFound/>
|
||||
<Route path="" view=|cx| view! {
|
||||
cx,
|
||||
<Todos/>
|
||||
}/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
@@ -198,14 +200,3 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn NotFound(cx: Scope) -> impl IntoView {
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
let resp = expect_context::<leptos_actix::ResponseOptions>(cx);
|
||||
resp.set_status(actix_web::http::StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
view! { cx, <h1>"Not Found"</h1> }
|
||||
}
|
||||
|
||||
26
flake.lock
generated
26
flake.lock
generated
@@ -38,11 +38,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1687898314,
|
||||
"narHash": "sha256-B4BHon3uMXQw8ZdbwxRK1BmxVOGBV4viipKpGaIlGwk=",
|
||||
"lastModified": 1681920287,
|
||||
"narHash": "sha256-+/d6XQQfhhXVfqfLROJoqj3TuG38CAeoT6jO1g9r1k0=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e18dc963075ed115afb3e312b64643bf8fd4b474",
|
||||
"rev": "645bc49f34fa8eff95479f0345ff57e55b53437e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -52,6 +52,22 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681358109,
|
||||
"narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
@@ -62,9 +78,7 @@
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1682043560,
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||
rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
@@ -23,8 +22,7 @@
|
||||
openssl
|
||||
pkg-config
|
||||
cacert
|
||||
mdbook
|
||||
(rust-bin.selectLatestNightlyWith(toolchain: toolchain.default.override {
|
||||
(rust-bin.selectLatestNightlyWith( toolchain: toolchain.default.override {
|
||||
extensions= [ "rust-src" "rust-analyzer" ];
|
||||
targets = [ "wasm32-unknown-unknown" ];
|
||||
}))
|
||||
|
||||
@@ -223,30 +223,26 @@ pub fn handle_server_fns_with_context(
|
||||
let res_options =
|
||||
use_context::<ResponseOptions>(cx).unwrap();
|
||||
|
||||
let mut res: HttpResponseBuilder =
|
||||
HttpResponse::Ok();
|
||||
let mut res: HttpResponseBuilder;
|
||||
let res_parts = res_options.0.write();
|
||||
|
||||
// if accept_header isn't set to one of these, it's a form submit
|
||||
// redirect back to the referrer if not redirect has been set
|
||||
if accept_header != Some("application/json")
|
||||
&& accept_header
|
||||
!= Some("application/x-www-form-urlencoded")
|
||||
&& accept_header != Some("application/cbor")
|
||||
if accept_header == Some("application/json")
|
||||
|| accept_header
|
||||
== Some("application/x-www-form-urlencoded")
|
||||
|| accept_header == Some("application/cbor")
|
||||
{
|
||||
// Location will already be set if redirect() has been used
|
||||
let has_location_set =
|
||||
res_parts.headers.get("Location").is_some();
|
||||
if !has_location_set {
|
||||
let referer = req
|
||||
.headers()
|
||||
.get("Referer")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or("/");
|
||||
res = HttpResponse::SeeOther();
|
||||
res.insert_header(("Location", referer))
|
||||
.content_type("application/json");
|
||||
}
|
||||
res = HttpResponse::Ok();
|
||||
}
|
||||
// otherwise, it's probably a <form> submit or something: redirect back to the referrer
|
||||
else {
|
||||
let referer = req
|
||||
.headers()
|
||||
.get("Referer")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or("/");
|
||||
res = HttpResponse::SeeOther();
|
||||
res.insert_header(("Location", referer))
|
||||
.content_type("application/json");
|
||||
};
|
||||
// Override StatusCode if it was set in a Resource or Element
|
||||
if let Some(status) = res_parts.status {
|
||||
|
||||
@@ -1276,24 +1276,18 @@ impl ExtractorHelper {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn extract<F, T, U, S>(
|
||||
&self,
|
||||
f: F,
|
||||
s: S,
|
||||
) -> Result<U, T::Rejection>
|
||||
pub async fn extract<F, T, U>(&self, f: F) -> Result<U, T::Rejection>
|
||||
where
|
||||
S: Sized,
|
||||
F: Extractor<T, U, S>,
|
||||
T: std::fmt::Debug + Send + FromRequestParts<S> + 'static,
|
||||
F: Extractor<T, U>,
|
||||
T: std::fmt::Debug + Send + FromRequestParts<()> + 'static,
|
||||
T::Rejection: std::fmt::Debug + Send + 'static,
|
||||
{
|
||||
let mut parts = self.parts.lock().await;
|
||||
let data = T::from_request_parts(&mut parts, &s).await?;
|
||||
let data = T::from_request_parts(&mut parts, &()).await?;
|
||||
Ok(f.call(data).await)
|
||||
}
|
||||
}
|
||||
|
||||
/// Getting ExtractorHelper from a request will return an ExtractorHelper whose state is ().
|
||||
impl<B> From<Request<B>> for ExtractorHelper {
|
||||
fn from(req: Request<B>) -> Self {
|
||||
// TODO provide body for extractors there, too?
|
||||
@@ -1330,71 +1324,34 @@ impl<B> From<Request<B>> for ExtractorHelper {
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub async fn extract<T, U>(
|
||||
cx: Scope,
|
||||
f: impl Extractor<T, U, ()>,
|
||||
f: impl Extractor<T, U>,
|
||||
) -> Result<U, T::Rejection>
|
||||
where
|
||||
T: std::fmt::Debug + Send + FromRequestParts<()> + 'static,
|
||||
T::Rejection: std::fmt::Debug + Send + 'static,
|
||||
{
|
||||
extract_with_state(cx, (), f).await
|
||||
}
|
||||
|
||||
/// A helper to make it easier to use Axum extractors in server functions. This takes
|
||||
/// a handler function and state as its arguments. The handler rules similar to Axum
|
||||
/// [handlers](https://docs.rs/axum/latest/axum/extract/index.html#intro): it is an async function
|
||||
/// whose arguments are “extractors.”
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[server(QueryExtract, "/api")]
|
||||
/// pub async fn query_extract(cx: Scope) -> Result<String, ServerFnError> {
|
||||
/// use axum::{extract::Query, http::Method};
|
||||
/// use leptos_axum::extract;
|
||||
/// let state: ServerState = use_context::<crate::ServerState>(cx)
|
||||
/// .ok_or(ServerFnError::ServerError("No server state".to_string()))?;
|
||||
///
|
||||
/// extract_with_state(cx, state, |method: Method, res: Query<MyQuery>| async move {
|
||||
/// format!("{method:?} and {}", res.q)
|
||||
/// },
|
||||
/// )
|
||||
/// .await
|
||||
/// .map_err(|e| ServerFnError::ServerError("Could not extract method and query...".to_string()))
|
||||
/// }
|
||||
/// ```
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub async fn extract_with_state<T, U, S>(
|
||||
cx: Scope,
|
||||
state: S,
|
||||
f: impl Extractor<T, U, S>,
|
||||
) -> Result<U, T::Rejection>
|
||||
where
|
||||
S: Sized,
|
||||
T: std::fmt::Debug + Send + FromRequestParts<S> + 'static,
|
||||
T::Rejection: std::fmt::Debug + Send + 'static,
|
||||
{
|
||||
use_context::<ExtractorHelper>(cx)
|
||||
.expect(
|
||||
"should have had ExtractorHelper provided by the leptos_axum \
|
||||
integration",
|
||||
)
|
||||
.extract(f, state)
|
||||
.extract(f)
|
||||
.await
|
||||
}
|
||||
|
||||
pub trait Extractor<T, U, S>
|
||||
pub trait Extractor<T, U>
|
||||
where
|
||||
S: Sized,
|
||||
T: FromRequestParts<S>,
|
||||
T: FromRequestParts<()>,
|
||||
{
|
||||
fn call(self, args: T) -> Pin<Box<dyn Future<Output = U>>>;
|
||||
}
|
||||
|
||||
macro_rules! factory_tuple ({ $($param:ident)* } => {
|
||||
impl<Func, Fut, U, S, $($param,)*> Extractor<($($param,)*), U, S> for Func
|
||||
impl<Func, Fut, U, $($param,)*> Extractor<($($param,)*), U> for Func
|
||||
where
|
||||
$($param: FromRequestParts<S> + Send,)*
|
||||
$($param: FromRequestParts<()> + Send,)*
|
||||
Func: FnOnce($($param),*) -> Fut + 'static,
|
||||
Fut: Future<Output = U> + 'static,
|
||||
S: Sized + Send + Sync,
|
||||
{
|
||||
#[inline]
|
||||
#[allow(non_snake_case)]
|
||||
|
||||
@@ -87,10 +87,10 @@ pub fn html_parts_separated(
|
||||
let output_name = &options.output_name;
|
||||
|
||||
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
|
||||
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME at compile time
|
||||
// Otherwise we need to add _bg because wasm_pack always does.
|
||||
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME
|
||||
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
|
||||
let mut wasm_output_name = output_name.clone();
|
||||
if std::option_env!("LEPTOS_OUTPUT_NAME").is_none() {
|
||||
if std::env::var("LEPTOS_OUTPUT_NAME").is_err() {
|
||||
wasm_output_name.push_str("_bg");
|
||||
}
|
||||
|
||||
|
||||
@@ -289,4 +289,4 @@ fn None(cx: Scope) -> impl IntoView {
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ use typed_builder::TypedBuilder;
|
||||
/// A Struct to allow us to parse LeptosOptions from the file. Not really needed, most interactions should
|
||||
/// occur with LeptosOptions
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct ConfFile {
|
||||
pub leptos_options: LeptosOptions,
|
||||
}
|
||||
@@ -24,37 +23,31 @@ pub struct ConfFile {
|
||||
/// correct path for WASM, JS, and Websockets, as well as other configuration tasks.
|
||||
/// It shares keys with cargo-leptos, to allow for easy interoperability
|
||||
#[derive(TypedBuilder, Debug, Clone, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct LeptosOptions {
|
||||
/// The name of the WASM and JS files generated by wasm-bindgen. Defaults to the crate name with underscores instead of dashes
|
||||
#[builder(setter(into))]
|
||||
pub output_name: String,
|
||||
/// The path of the all the files generated by cargo-leptos. This defaults to '.' for convenience when integrating with other
|
||||
/// tools.
|
||||
#[builder(setter(into), default=default_site_root())]
|
||||
#[serde(default = "default_site_root")]
|
||||
#[builder(setter(into), default=".".to_string())]
|
||||
pub site_root: String,
|
||||
/// The path of the WASM and JS files generated by wasm-bindgen from the root of your app
|
||||
/// By default, wasm-bindgen puts them in `pkg`.
|
||||
#[builder(setter(into), default=default_site_pkg_dir())]
|
||||
#[serde(default = "default_site_pkg_dir")]
|
||||
#[builder(setter(into), default="pkg".to_string())]
|
||||
pub site_pkg_dir: String,
|
||||
/// Used to configure the running environment of Leptos. Can be used to load dev constants and keys v prod, or change
|
||||
/// things based on the deployment environment
|
||||
/// I recommend passing in the result of `env::var("LEPTOS_ENV")`
|
||||
#[builder(setter(into), default=default_env())]
|
||||
#[serde(default = "default_env")]
|
||||
#[builder(setter(into), default=Env::DEV)]
|
||||
pub env: Env,
|
||||
/// Provides a way to control the address leptos is served from.
|
||||
/// Using an env variable here would allow you to run the same code in dev and prod
|
||||
/// Defaults to `127.0.0.1:3000`
|
||||
#[builder(setter(into), default=default_site_addr())]
|
||||
#[serde(default = "default_site_addr")]
|
||||
#[builder(setter(into), default=SocketAddr::from(([127,0,0,1], 3000)))]
|
||||
pub site_addr: SocketAddr,
|
||||
/// The port the Websocket watcher listens on. Should match the `reload_port` in cargo-leptos(if using).
|
||||
/// Defaults to `3001`
|
||||
#[builder(default = default_reload_port())]
|
||||
#[serde(default = "default_reload_port")]
|
||||
#[builder(default = 3001)]
|
||||
pub reload_port: u32,
|
||||
}
|
||||
|
||||
@@ -88,26 +81,6 @@ impl LeptosOptions {
|
||||
}
|
||||
}
|
||||
|
||||
fn default_site_root() -> String {
|
||||
".".to_string()
|
||||
}
|
||||
|
||||
fn default_site_pkg_dir() -> String {
|
||||
"pkg".to_string()
|
||||
}
|
||||
|
||||
fn default_env() -> Env {
|
||||
Env::DEV
|
||||
}
|
||||
|
||||
fn default_site_addr() -> SocketAddr {
|
||||
SocketAddr::from(([127, 0, 0, 1], 3000))
|
||||
}
|
||||
|
||||
fn default_reload_port() -> u32 {
|
||||
3001
|
||||
}
|
||||
|
||||
fn env_w_default(
|
||||
key: &str,
|
||||
default: &str,
|
||||
@@ -122,7 +95,7 @@ fn env_w_default(
|
||||
/// An enum that can be used to define the environment Leptos is running in.
|
||||
/// Setting this to the `PROD` variant will not include the WebSocket code for `cargo-leptos` watch mode.
|
||||
/// Defaults to `DEV`.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
pub enum Env {
|
||||
PROD,
|
||||
DEV,
|
||||
@@ -202,7 +175,9 @@ pub fn get_config_from_str(text: &str) -> Result<ConfFile, LeptosConfigError> {
|
||||
// so that serde error messages have right line number
|
||||
let newlines = text[..start].matches('\n').count();
|
||||
let input = "\n".repeat(newlines) + &text[start..];
|
||||
let toml = input.replace(metadata_name, "[leptos-options]");
|
||||
let toml = input
|
||||
.replace(metadata_name, "[leptos_options]")
|
||||
.replace('-', "_");
|
||||
let settings = Config::builder()
|
||||
// Read the "default" configuration file
|
||||
.add_source(File::from_str(&toml, FileFormat::Toml))
|
||||
|
||||
@@ -46,7 +46,7 @@ async fn get_configuration_from_file_ok() {
|
||||
.unwrap()
|
||||
.leptos_options;
|
||||
|
||||
assert_eq!(config.output_name, "app-test");
|
||||
assert_eq!(config.output_name, "app_test");
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
assert_eq!(
|
||||
@@ -93,7 +93,7 @@ async fn get_config_from_file_ok() {
|
||||
.unwrap()
|
||||
.leptos_options;
|
||||
|
||||
assert_eq!(config.output_name, "app-test");
|
||||
assert_eq!(config.output_name, "app_test");
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
assert_eq!(
|
||||
@@ -128,7 +128,7 @@ fn get_config_from_str_content() {
|
||||
let config = get_config_from_str(CARGO_TOML_CONTENT_OK)
|
||||
.unwrap()
|
||||
.leptos_options;
|
||||
assert_eq!(config.output_name, "app-test");
|
||||
assert_eq!(config.output_name, "app_test");
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
assert_eq!(
|
||||
@@ -141,14 +141,14 @@ fn get_config_from_str_content() {
|
||||
#[tokio::test]
|
||||
async fn get_config_from_env() {
|
||||
// Test config values from environment variables
|
||||
std::env::set_var("LEPTOS_OUTPUT_NAME", "app-test");
|
||||
std::env::set_var("LEPTOS_OUTPUT_NAME", "app_test");
|
||||
std::env::set_var("LEPTOS_SITE_ROOT", "my_target/site");
|
||||
std::env::set_var("LEPTOS_SITE_PKG_DIR", "my_pkg");
|
||||
std::env::set_var("LEPTOS_SITE_ADDR", "0.0.0.0:80");
|
||||
std::env::set_var("LEPTOS_RELOAD_PORT", "8080");
|
||||
|
||||
let config = get_configuration(None).await.unwrap().leptos_options;
|
||||
assert_eq!(config.output_name, "app-test");
|
||||
assert_eq!(config.output_name, "app_test");
|
||||
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
@@ -176,8 +176,8 @@ async fn get_config_from_env() {
|
||||
|
||||
#[test]
|
||||
fn leptos_options_builder_default() {
|
||||
let conf = LeptosOptions::builder().output_name("app-test").build();
|
||||
assert_eq!(conf.output_name, "app-test");
|
||||
let conf = LeptosOptions::builder().output_name("app_test").build();
|
||||
assert_eq!(conf.output_name, "app_test");
|
||||
assert!(matches!(conf.env, Env::DEV));
|
||||
assert_eq!(conf.site_pkg_dir, "pkg");
|
||||
assert_eq!(conf.site_root, ".");
|
||||
|
||||
@@ -14,7 +14,7 @@ drain_filter_polyfill = "0.1"
|
||||
educe = "0.4"
|
||||
futures = "0.3"
|
||||
html-escape = "0.2"
|
||||
indexmap = "2"
|
||||
indexmap = "1.9"
|
||||
itertools = "0.10"
|
||||
js-sys = "0.3"
|
||||
leptos_reactive = { workspace = true }
|
||||
@@ -49,7 +49,6 @@ features = [
|
||||
"Range",
|
||||
"Text",
|
||||
"HtmlCollection",
|
||||
"ShadowRoot",
|
||||
"TreeWalker",
|
||||
|
||||
# Events we cast to in leptos_macro -- added here so we don't force users to import them
|
||||
|
||||
@@ -55,8 +55,6 @@ fn view_fn(cx: Scope) -> impl IntoView {
|
||||
<hr/>
|
||||
<Test from=[1, 4, 3, 2, 5] to=[1, 2, 3, 4, 5]/>
|
||||
<Test from=[4, 5, 3, 1, 2] to=[1, 2, 3, 4, 5]/>
|
||||
<Test from=[0, 1, 2, 3] to=[1, 3]/> // issue #1274
|
||||
<Test from=[] to=[3, 9, 17] then=vec![3, 5, 7, 9, 17, 23]/> // issue #1297
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,11 @@ mod web {
|
||||
};
|
||||
pub use drain_filter_polyfill::VecExt as VecDrainFilterExt;
|
||||
pub use leptos_reactive::create_effect;
|
||||
pub use once_cell::unsync::OnceCell;
|
||||
pub use std::cell::OnceCell;
|
||||
pub use wasm_bindgen::JsCast;
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // not used in SSR
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
type FxIndexSet<T> =
|
||||
indexmap::IndexSet<T, std::hash::BuildHasherDefault<rustc_hash::FxHasher>>;
|
||||
|
||||
@@ -230,12 +230,7 @@ impl EachItem {
|
||||
fragment.append_with_node_1(&closing.node).unwrap();
|
||||
}
|
||||
|
||||
// if child view is Text and if we are hydrating, we do not
|
||||
// need to mount it. otherwise, mount it here
|
||||
if !HydrationCtx::is_hydrating() || !matches!(child, View::Text(_))
|
||||
{
|
||||
mount_child(MountKind::Before(&closing.node), &child);
|
||||
}
|
||||
mount_child(MountKind::Before(&closing.node), &child);
|
||||
|
||||
Some(fragment)
|
||||
} else {
|
||||
@@ -499,8 +494,8 @@ where
|
||||
#[educe(Debug)]
|
||||
struct HashRun<T>(#[educe(Debug(ignore))] T);
|
||||
|
||||
/// Calculates the operations needed to get from `from` to `to`.
|
||||
#[allow(dead_code)] // not used in SSR but useful to have available for testing
|
||||
/// Calculates the operations need to get from `a` to `b`.
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
fn diff<K: Eq + Hash>(from: &FxIndexSet<K>, to: &FxIndexSet<K>) -> Diff {
|
||||
if from.is_empty() && to.is_empty() {
|
||||
return Diff::default();
|
||||
@@ -523,90 +518,207 @@ fn diff<K: Eq + Hash>(from: &FxIndexSet<K>, to: &FxIndexSet<K>) -> Diff {
|
||||
};
|
||||
}
|
||||
|
||||
let mut removed = vec![];
|
||||
let mut moved = vec![];
|
||||
let mut added = vec![];
|
||||
let max_len = std::cmp::max(from.len(), to.len());
|
||||
// Get removed items
|
||||
let removed = from.difference(to);
|
||||
|
||||
for index in 0..max_len {
|
||||
let from_item = from.get_index(index);
|
||||
let to_item = to.get_index(index);
|
||||
let remove_cmds = removed
|
||||
.clone()
|
||||
.map(|k| from.get_full(k).unwrap().0)
|
||||
.map(|idx| DiffOpRemove { at: idx });
|
||||
|
||||
// if they're the same, do nothing
|
||||
if from_item != to_item {
|
||||
// if it's only in old, not new, remove it
|
||||
if from_item.is_some() && !to.contains(from_item.unwrap()) {
|
||||
let op = DiffOpRemove { at: index };
|
||||
removed.push(op);
|
||||
}
|
||||
// if it's only in new, not old, add it
|
||||
if to_item.is_some() && !from.contains(to_item.unwrap()) {
|
||||
let op = DiffOpAdd {
|
||||
at: index,
|
||||
mode: DiffOpAddMode::Normal,
|
||||
};
|
||||
added.push(op);
|
||||
}
|
||||
// if it's in both old and new, it can either
|
||||
// 1) be moved (and need to move in the DOM)
|
||||
// 2) be moved (but not need to move in the DOM)
|
||||
// * this would happen if, for example, 2 items
|
||||
// have been added before it, and it has moved by 2
|
||||
if let Some(from_item) = from_item {
|
||||
if let Some(to_item) = to.get_full(from_item) {
|
||||
let moves_forward_by = (to_item.0 as i32) - (index as i32);
|
||||
let move_in_dom = moves_forward_by
|
||||
!= (added.len() as i32) - (removed.len() as i32);
|
||||
// Get added items
|
||||
let added = to.difference(from);
|
||||
|
||||
let op = DiffOpMove {
|
||||
from: index,
|
||||
len: 1,
|
||||
to: to_item.0,
|
||||
move_in_dom,
|
||||
};
|
||||
moved.push(op);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let add_cmds =
|
||||
added
|
||||
.clone()
|
||||
.map(|k| to.get_full(k).unwrap().0)
|
||||
.map(|idx| DiffOpAdd {
|
||||
at: idx,
|
||||
mode: Default::default(),
|
||||
});
|
||||
|
||||
moved = group_adjacent_moves(moved);
|
||||
// Get items that might have moved
|
||||
let from_moved = from.intersection(&to).collect::<FxIndexSet<_>>();
|
||||
let to_moved = to.intersection(&from).collect::<FxIndexSet<_>>();
|
||||
|
||||
Diff {
|
||||
removed,
|
||||
items_to_move: moved.iter().map(|m| m.len).sum(),
|
||||
moved,
|
||||
added,
|
||||
let move_cmds = find_ranges(from_moved, to_moved, from, to);
|
||||
|
||||
let mut diff = Diff {
|
||||
removed: remove_cmds.collect(),
|
||||
items_to_move: move_cmds.iter().map(|range| range.len).sum(),
|
||||
moved: move_cmds,
|
||||
added: add_cmds.collect(),
|
||||
clear: false,
|
||||
};
|
||||
|
||||
apply_opts(from, to, &mut diff);
|
||||
|
||||
#[cfg(test)]
|
||||
{
|
||||
let mut adds_sorted = diff.added.clone();
|
||||
adds_sorted.sort_unstable_by_key(|add| add.at);
|
||||
|
||||
assert_eq!(diff.added, adds_sorted, "adds must be sorted");
|
||||
|
||||
let mut moves_sorted = diff.moved.clone();
|
||||
moves_sorted.sort_unstable_by_key(|move_| move_.to);
|
||||
|
||||
assert_eq!(diff.moved, moves_sorted, "moves must be sorted by `to`");
|
||||
}
|
||||
|
||||
diff
|
||||
}
|
||||
|
||||
/// Group adjacent items that are being moved as a group.
|
||||
/// For example from `[2, 3, 5, 6]` to `[1, 2, 3, 4, 5, 6]` should result
|
||||
/// in a move for `2,3` and `5,6` rather than 4 individual moves.
|
||||
fn group_adjacent_moves(moved: Vec<DiffOpMove>) -> Vec<DiffOpMove> {
|
||||
let mut prev: Option<DiffOpMove> = None;
|
||||
let mut new_moved = Vec::with_capacity(moved.len());
|
||||
for m in moved {
|
||||
match prev {
|
||||
Some(mut p) => {
|
||||
if (m.from == p.from + p.len) && (m.to == p.to + p.len) {
|
||||
p.len += 1;
|
||||
prev = Some(p);
|
||||
} else {
|
||||
new_moved.push(prev.take().unwrap());
|
||||
prev = Some(m);
|
||||
}
|
||||
}
|
||||
None => prev = Some(m),
|
||||
/// Builds and returns the ranges of items that need to
|
||||
/// move sorted by `to`.
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
fn find_ranges<K: Eq + Hash>(
|
||||
from_moved: FxIndexSet<&K>,
|
||||
to_moved: FxIndexSet<&K>,
|
||||
from: &FxIndexSet<K>,
|
||||
to: &FxIndexSet<K>,
|
||||
) -> Vec<DiffOpMove> {
|
||||
let mut ranges = Vec::with_capacity(from.len());
|
||||
let mut prev_to_moved_index = 0;
|
||||
let mut range = DiffOpMove::default();
|
||||
|
||||
for (i, k) in from_moved.into_iter().enumerate() {
|
||||
let to_moved_index = to_moved.get_index_of(k).unwrap();
|
||||
|
||||
if i == 0 {
|
||||
range.from = from.get_index_of(k).unwrap();
|
||||
range.to = to.get_index_of(k).unwrap();
|
||||
}
|
||||
// The range continues
|
||||
else if to_moved_index == prev_to_moved_index + 1 {
|
||||
range.len += 1;
|
||||
}
|
||||
// We're done with this range, start a new one
|
||||
else {
|
||||
ranges.push(std::mem::take(&mut range));
|
||||
|
||||
range.from = from.get_index_of(k).unwrap();
|
||||
range.to = to.get_index_of(k).unwrap();
|
||||
}
|
||||
|
||||
prev_to_moved_index = to_moved_index;
|
||||
}
|
||||
|
||||
ranges.push(std::mem::take(&mut range));
|
||||
|
||||
// We need to remove ranges that didn't move relative to each other
|
||||
// as well as marking items that don't need to move in the DOM
|
||||
let mut to_ranges = ranges.clone();
|
||||
to_ranges.sort_unstable_by_key(|range| range.to);
|
||||
|
||||
let mut filtered_ranges = vec![];
|
||||
|
||||
let to_ranges_len = to_ranges.len();
|
||||
|
||||
for (i, range) in to_ranges.into_iter().enumerate() {
|
||||
if range != ranges[i] {
|
||||
filtered_ranges.push(range);
|
||||
}
|
||||
// The item did move, just not in the DOM
|
||||
else if range.from != range.to {
|
||||
filtered_ranges.push(DiffOpMove {
|
||||
move_in_dom: false,
|
||||
..range
|
||||
});
|
||||
} else if to_ranges_len > 2 {
|
||||
// TODO: Remove this else case...this is one of the biggest
|
||||
// optimizations we can do, but we're skipping this right now
|
||||
// until we figure out a way to handle moving around ranges
|
||||
// that did not move
|
||||
filtered_ranges.push(range);
|
||||
}
|
||||
}
|
||||
if let Some(prev) = prev {
|
||||
new_moved.push(prev)
|
||||
}
|
||||
new_moved
|
||||
|
||||
filtered_ranges
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
fn apply_opts<K: Eq + Hash>(
|
||||
from: &FxIndexSet<K>,
|
||||
to: &FxIndexSet<K>,
|
||||
cmds: &mut Diff,
|
||||
) {
|
||||
optimize_moves(&mut cmds.moved);
|
||||
|
||||
// We can optimize the case of replacing all items
|
||||
if !from.is_empty()
|
||||
&& !to.is_empty()
|
||||
&& cmds.removed.len() == from.len()
|
||||
&& cmds.moved.is_empty()
|
||||
{
|
||||
cmds.clear = true;
|
||||
cmds.removed.clear();
|
||||
|
||||
cmds.added
|
||||
.iter_mut()
|
||||
.for_each(|op| op.mode = DiffOpAddMode::Append);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// We can optimize appends.
|
||||
if !cmds.added.is_empty()
|
||||
&& cmds.moved.is_empty()
|
||||
&& cmds.removed.is_empty()
|
||||
&& cmds.added[0].at >= from.len()
|
||||
{
|
||||
cmds.added
|
||||
.iter_mut()
|
||||
.for_each(|op| op.mode = DiffOpAddMode::Append);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
fn optimize_moves(moves: &mut Vec<DiffOpMove>) {
|
||||
if moves.is_empty() || moves.len() == 1 {
|
||||
// Do nothing
|
||||
}
|
||||
// This is the easiest optimal move case, which is to
|
||||
// simply swap the 2 ranges. We only need to move the range
|
||||
// that is smallest.
|
||||
else if moves.len() == 2 {
|
||||
if moves[1].len < moves[0].len {
|
||||
moves[0].move_in_dom = false;
|
||||
} else {
|
||||
moves[1].move_in_dom = false;
|
||||
}
|
||||
}
|
||||
// Interestingly enoughs, there are NO configuration that are possible
|
||||
// for ranges of 3.
|
||||
//
|
||||
// For example, take A, B, C. Here are all possible configurations and
|
||||
// reasons for why they are impossible:
|
||||
// - A B C # identity, would be removed by ranges that didn't move
|
||||
// - A C B # `A` would be removed, thus it's a case of length 2
|
||||
// - B A C # `C` would be removed, thus it's a case of length 2
|
||||
// - B C A # `B C` are congiguous, so this is would have been a single range
|
||||
// - C A B # `A B` are congiguous, so this is would have been a single range
|
||||
// - C B A # `B` would be removed, thus it's a case of length 2
|
||||
//
|
||||
// We can add more pre-computed tables here if benchmarking or
|
||||
// user demand needs it...nevertheless, it is unlikely for us
|
||||
// to implement this algorithm to handle N ranges, because this
|
||||
// becomes exponentially more expensive to compute. It's faster,
|
||||
// for the most part, to assume the ranges are random and move
|
||||
// all the ranges around than to try and figure out the best way
|
||||
// to move them
|
||||
else {
|
||||
// The idea here is that for N ranges, we never need to
|
||||
// move the largest range, rather, have all ranges move
|
||||
// around it.
|
||||
let move_ = moves.iter_mut().max_by_key(|move_| move_.len).unwrap();
|
||||
|
||||
move_.move_in_dom = false;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
struct Diff {
|
||||
removed: Vec<DiffOpRemove>,
|
||||
@@ -616,6 +728,7 @@ struct Diff {
|
||||
clear: bool,
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
struct DiffOpMove {
|
||||
/// The index this range is starting relative to `from`.
|
||||
@@ -629,6 +742,7 @@ struct DiffOpMove {
|
||||
move_in_dom: bool,
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
impl Default for DiffOpMove {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -640,18 +754,20 @@ impl Default for DiffOpMove {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
struct DiffOpAdd {
|
||||
at: usize,
|
||||
mode: DiffOpAddMode,
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct DiffOpRemove {
|
||||
at: usize,
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // Append not used in SSR but useful
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum DiffOpAddMode {
|
||||
Normal,
|
||||
@@ -660,6 +776,7 @@ enum DiffOpAddMode {
|
||||
_Prepend,
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
impl Default for DiffOpAddMode {
|
||||
fn default() -> Self {
|
||||
Self::Normal
|
||||
@@ -869,173 +986,404 @@ fn unpack_moves(diff: &Diff) -> (Vec<DiffOpMove>, Vec<DiffOpAdd>) {
|
||||
(moves, adds)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_utils {
|
||||
use super::*;
|
||||
// #[cfg(test)]
|
||||
// mod test_utils {
|
||||
// use super::*;
|
||||
|
||||
pub trait IntoFxIndexSet<K> {
|
||||
fn into_fx_index_set(self) -> FxIndexSet<K>;
|
||||
}
|
||||
// pub trait IntoFxIndexSet<K> {
|
||||
// fn into_fx_index_set(self) -> FxIndexSet<K>;
|
||||
// }
|
||||
|
||||
impl<T, K> IntoFxIndexSet<K> for T
|
||||
where
|
||||
T: IntoIterator<Item = K>,
|
||||
K: Eq + Hash,
|
||||
{
|
||||
fn into_fx_index_set(self) -> FxIndexSet<K> {
|
||||
self.into_iter().collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
// impl<T, K> IntoFxIndexSet<K> for T
|
||||
// where
|
||||
// T: IntoIterator<Item = K>,
|
||||
// K: Eq + Hash,
|
||||
// {
|
||||
// fn into_fx_index_set(self) -> FxIndexSet<K> {
|
||||
// self.into_iter().collect()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
#[cfg(test)]
|
||||
use test_utils::*;
|
||||
// #[cfg(test)]
|
||||
// use test_utils::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod diff {
|
||||
use super::*;
|
||||
// #[cfg(test)]
|
||||
// mod find_ranges {
|
||||
// use super::*;
|
||||
|
||||
#[test]
|
||||
fn only_adds() {
|
||||
let diff =
|
||||
diff(&[].into_fx_index_set(), &[1, 2, 3].into_fx_index_set());
|
||||
// // Single range tests will be empty because of removing ranges
|
||||
// // that didn't move
|
||||
// #[test]
|
||||
// fn single_range() {
|
||||
// let ranges = find_ranges(
|
||||
// [1, 2, 3, 4].iter().into_fx_index_set(),
|
||||
// [1, 2, 3, 4].iter().into_fx_index_set(),
|
||||
// &[1, 2, 3, 4].into_fx_index_set(),
|
||||
// &[1, 2, 3, 4].into_fx_index_set(),
|
||||
// );
|
||||
|
||||
assert_eq!(
|
||||
diff,
|
||||
Diff {
|
||||
added: vec![
|
||||
DiffOpAdd {
|
||||
at: 0,
|
||||
mode: DiffOpAddMode::Append
|
||||
},
|
||||
DiffOpAdd {
|
||||
at: 1,
|
||||
mode: DiffOpAddMode::Append
|
||||
},
|
||||
DiffOpAdd {
|
||||
at: 2,
|
||||
mode: DiffOpAddMode::Append
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
// assert_eq!(ranges, vec![]);
|
||||
// }
|
||||
|
||||
#[test]
|
||||
fn only_removes() {
|
||||
let diff =
|
||||
diff(&[1, 2, 3].into_fx_index_set(), &[3].into_fx_index_set());
|
||||
// #[test]
|
||||
// fn single_range_with_adds() {
|
||||
// let ranges = find_ranges(
|
||||
// [1, 2, 3, 4].iter().into_fx_index_set(),
|
||||
// [1, 2, 3, 4].iter().into_fx_index_set(),
|
||||
// &[1, 2, 3, 4].into_fx_index_set(),
|
||||
// &[1, 2, 5, 3, 4].into_fx_index_set(),
|
||||
// );
|
||||
|
||||
assert_eq!(
|
||||
diff,
|
||||
Diff {
|
||||
removed: vec![DiffOpRemove { at: 0 }, DiffOpRemove { at: 1 }],
|
||||
moved: vec![DiffOpMove {
|
||||
from: 2,
|
||||
len: 1,
|
||||
to: 0,
|
||||
move_in_dom: false
|
||||
}],
|
||||
items_to_move: 1,
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
// assert_eq!(ranges, vec![]);
|
||||
// }
|
||||
|
||||
#[test]
|
||||
fn adds_with_no_move() {
|
||||
let diff =
|
||||
diff(&[3].into_fx_index_set(), &[1, 2, 3].into_fx_index_set());
|
||||
// #[test]
|
||||
// fn single_range_with_removals() {
|
||||
// let ranges = find_ranges(
|
||||
// [1, 2, 3, 4].iter().into_fx_index_set(),
|
||||
// [1, 2, 3, 4].iter().into_fx_index_set(),
|
||||
// &[1, 2, 5, 3, 4].into_fx_index_set(),
|
||||
// &[1, 2, 3, 4].into_fx_index_set(),
|
||||
// );
|
||||
|
||||
assert_eq!(
|
||||
diff,
|
||||
Diff {
|
||||
added: vec![
|
||||
DiffOpAdd {
|
||||
at: 0,
|
||||
..Default::default()
|
||||
},
|
||||
DiffOpAdd {
|
||||
at: 1,
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
moved: vec![DiffOpMove {
|
||||
from: 0,
|
||||
len: 1,
|
||||
to: 2,
|
||||
move_in_dom: true
|
||||
}],
|
||||
items_to_move: 1,
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
// assert_eq!(ranges, vec![]);
|
||||
// }
|
||||
|
||||
#[test]
|
||||
fn move_as_group() {
|
||||
let diff = diff(
|
||||
&[2, 3, 4, 5].into_fx_index_set(),
|
||||
&[1, 2, 3, 4, 5].into_fx_index_set(),
|
||||
);
|
||||
// #[test]
|
||||
// fn two_ranges() {
|
||||
// let ranges = find_ranges(
|
||||
// [1, 2, 3, 4].iter().into_fx_index_set(),
|
||||
// [3, 4, 1, 2].iter().into_fx_index_set(),
|
||||
// &[1, 2, 3, 4].into_fx_index_set(),
|
||||
// &[3, 4, 1, 2].into_fx_index_set(),
|
||||
// );
|
||||
|
||||
assert_eq!(
|
||||
diff,
|
||||
Diff {
|
||||
added: vec![DiffOpAdd {
|
||||
at: 0,
|
||||
..Default::default()
|
||||
},],
|
||||
moved: vec![DiffOpMove {
|
||||
from: 0,
|
||||
len: 4,
|
||||
to: 1,
|
||||
move_in_dom: false
|
||||
},],
|
||||
items_to_move: 4,
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
// assert_eq!(
|
||||
// ranges,
|
||||
// vec![
|
||||
// DiffOpMove {
|
||||
// from: 2,
|
||||
// to: 0,
|
||||
// len: 2,
|
||||
// move_in_dom: true,
|
||||
// },
|
||||
// DiffOpMove {
|
||||
// from: 0,
|
||||
// to: 2,
|
||||
// len: 2,
|
||||
// move_in_dom: true,
|
||||
// },
|
||||
// ]
|
||||
// );
|
||||
// }
|
||||
|
||||
#[test]
|
||||
fn move_as_group_with_gap() {
|
||||
let diff = diff(
|
||||
&[2, 3, 5, 6].into_fx_index_set(),
|
||||
&[1, 2, 3, 4, 5, 6].into_fx_index_set(),
|
||||
);
|
||||
// #[test]
|
||||
// fn two_ranges_with_adds() {
|
||||
// let ranges = find_ranges(
|
||||
// [1, 2, 3, 4].iter().into_fx_index_set(),
|
||||
// [3, 4, 1, 2].iter().into_fx_index_set(),
|
||||
// &[1, 2, 3, 4].into_fx_index_set(),
|
||||
// &[3, 4, 5, 1, 6, 2].into_fx_index_set(),
|
||||
// );
|
||||
|
||||
assert_eq!(
|
||||
diff,
|
||||
Diff {
|
||||
added: vec![
|
||||
DiffOpAdd {
|
||||
at: 0,
|
||||
..Default::default()
|
||||
},
|
||||
DiffOpAdd {
|
||||
at: 3,
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
moved: vec![
|
||||
DiffOpMove {
|
||||
from: 0,
|
||||
len: 2,
|
||||
to: 1,
|
||||
move_in_dom: false
|
||||
},
|
||||
DiffOpMove {
|
||||
from: 2,
|
||||
len: 2,
|
||||
to: 4,
|
||||
move_in_dom: true
|
||||
}
|
||||
],
|
||||
items_to_move: 4,
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
// assert_eq!(
|
||||
// ranges,
|
||||
// vec![
|
||||
// DiffOpMove {
|
||||
// from: 2,
|
||||
// to: 0,
|
||||
// len: 2,
|
||||
// },
|
||||
// DiffOpMove {
|
||||
// from: 0,
|
||||
// to: 3,
|
||||
// len: 2,
|
||||
// },
|
||||
// ]
|
||||
// );
|
||||
// }
|
||||
// #[test]
|
||||
// fn two_ranges_with_removals() {
|
||||
// let ranges = find_ranges(
|
||||
// [1, 2, 3, 4].iter().into_fx_index_set(),
|
||||
// [3, 4, 1, 2].iter().into_fx_index_set(),
|
||||
// &[1, 5, 2, 6, 3, 4].into_fx_index_set(),
|
||||
// &[3, 4, 1, 2].into_fx_index_set(),
|
||||
// );
|
||||
|
||||
// assert_eq!(
|
||||
// ranges,
|
||||
// vec![
|
||||
// DiffOpMove {
|
||||
// from: 4,
|
||||
// to: 0,
|
||||
// len: 2,
|
||||
// },
|
||||
// DiffOpMove {
|
||||
// from: 0,
|
||||
// to: 2,
|
||||
// len: 2,
|
||||
// },
|
||||
// ]
|
||||
// );
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn remove_ranges_that_did_not_move() {
|
||||
// // Here, 'C' doesn't change
|
||||
// let ranges = find_ranges(
|
||||
// ['A', 'B', 'C', 'D'].iter().into_fx_index_set(),
|
||||
// ['B', 'D', 'C', 'A'].iter().into_fx_index_set(),
|
||||
// &['A', 'B', 'C', 'D'].into_fx_index_set(),
|
||||
// &['B', 'D', 'C', 'A'].into_fx_index_set(),
|
||||
// );
|
||||
|
||||
// assert_eq!(
|
||||
// ranges,
|
||||
// vec![
|
||||
// DiffOpMove {
|
||||
// from: 1,
|
||||
// to: 0,
|
||||
// len: 1,
|
||||
// },
|
||||
// DiffOpMove {
|
||||
// from: 3,
|
||||
// to: 1,
|
||||
// len: 1,
|
||||
// },
|
||||
// DiffOpMove {
|
||||
// from: 0,
|
||||
// to: 3,
|
||||
// len: 1,
|
||||
// },
|
||||
// ]
|
||||
// );
|
||||
|
||||
// // Now we're going to to the same as above, just with more items
|
||||
// //
|
||||
// // A = 1
|
||||
// // B = 2, 3
|
||||
// // C = 4, 5, 6
|
||||
// // D = 7, 8, 9, 0
|
||||
|
||||
// let ranges = find_ranges(
|
||||
// //A B C D
|
||||
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 0].iter().into_fx_index_set(),
|
||||
// //B D C A
|
||||
// [2, 3, 7, 8, 9, 0, 4, 5, 6, 1].iter().into_fx_index_set(),
|
||||
// //A B C D
|
||||
// &[1, 2, 3, 4, 5, 6, 7, 8, 9, 0].into_fx_index_set(),
|
||||
// //B D C A
|
||||
// &[2, 3, 7, 8, 9, 0, 4, 5, 6, 1].into_fx_index_set(),
|
||||
// );
|
||||
|
||||
// assert_eq!(
|
||||
// ranges,
|
||||
// vec![
|
||||
// DiffOpMove {
|
||||
// from: 1,
|
||||
// to: 0,
|
||||
// len: 2,
|
||||
// },
|
||||
// DiffOpMove {
|
||||
// from: 6,
|
||||
// to: 2,
|
||||
// len: 4,
|
||||
// },
|
||||
// DiffOpMove {
|
||||
// from: 0,
|
||||
// to: 9,
|
||||
// len: 1,
|
||||
// },
|
||||
// ]
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod optimize_moves {
|
||||
// use super::*;
|
||||
|
||||
// #[test]
|
||||
// fn swap() {
|
||||
// let mut moves = vec![
|
||||
// DiffOpMove {
|
||||
// from: 0,
|
||||
// to: 6,
|
||||
// len: 2,
|
||||
// ..Default::default()
|
||||
// },
|
||||
// DiffOpMove {
|
||||
// from: 6,
|
||||
// to: 0,
|
||||
// len: 7,
|
||||
// ..Default::default()
|
||||
// },
|
||||
// ];
|
||||
|
||||
// optimize_moves(&mut moves);
|
||||
|
||||
// assert_eq!(
|
||||
// moves,
|
||||
// vec![DiffOpMove {
|
||||
// from: 0,
|
||||
// to: 6,
|
||||
// len: 2,
|
||||
// ..Default::default()
|
||||
// }]
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod add_or_move {
|
||||
// use super::*;
|
||||
|
||||
// #[test]
|
||||
// fn simple_range() {
|
||||
// let cmds = AddOrMove::from_diff(&Diff {
|
||||
// moved: vec![DiffOpMove {
|
||||
// from: 0,
|
||||
// to: 0,
|
||||
// len: 3,
|
||||
// }],
|
||||
// ..Default::default()
|
||||
// });
|
||||
|
||||
// assert_eq!(
|
||||
// cmds,
|
||||
// vec![
|
||||
// DiffOpMove {
|
||||
// from: 0,
|
||||
// to: 0,
|
||||
// len: 1,
|
||||
// },
|
||||
// DiffOpMove {
|
||||
// from: 1,
|
||||
// to: 1,
|
||||
// len: 1,
|
||||
// },
|
||||
// DiffOpMove {
|
||||
// from: 2,
|
||||
// to: 2,
|
||||
// len: 1,
|
||||
// },
|
||||
// ]
|
||||
// );
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn range_with_add() {
|
||||
// let cmds = AddOrMove::from_diff(&Diff {
|
||||
// moved: vec![DiffOpMove {
|
||||
// from: 0,
|
||||
// to: 0,
|
||||
// len: 3,
|
||||
// move_in_dom: true,
|
||||
// }],
|
||||
// added: vec![DiffOpAdd {
|
||||
// at: 2,
|
||||
// ..Default::default()
|
||||
// }],
|
||||
// ..Default::default()
|
||||
// });
|
||||
|
||||
// assert_eq!(
|
||||
// cmds,
|
||||
// vec![
|
||||
// AddOrMove::Move(DiffOpMove {
|
||||
// from: 0,
|
||||
// to: 0,
|
||||
// len: 1,
|
||||
// move_in_dom: true,
|
||||
// }),
|
||||
// AddOrMove::Move(DiffOpMove {
|
||||
// from: 1,
|
||||
// to: 1,
|
||||
// len: 1,
|
||||
// move_in_dom: true,
|
||||
// }),
|
||||
// AddOrMove::Add(DiffOpAdd {
|
||||
// at: 2,
|
||||
// ..Default::default()
|
||||
// }),
|
||||
// AddOrMove::Move(DiffOpMove {
|
||||
// from: 3,
|
||||
// to: 3,
|
||||
// len: 1,
|
||||
// move_in_dom: true,
|
||||
// }),
|
||||
// ]
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod diff {
|
||||
// use super::*;
|
||||
|
||||
// #[test]
|
||||
// fn only_adds() {
|
||||
// let diff =
|
||||
// diff(&[].into_fx_index_set(), &[1, 2, 3].into_fx_index_set());
|
||||
|
||||
// assert_eq!(
|
||||
// diff,
|
||||
// Diff {
|
||||
// added: vec![
|
||||
// DiffOpAdd {
|
||||
// at: 0,
|
||||
// mode: DiffOpAddMode::Append
|
||||
// },
|
||||
// DiffOpAdd {
|
||||
// at: 1,
|
||||
// mode: DiffOpAddMode::Append
|
||||
// },
|
||||
// DiffOpAdd {
|
||||
// at: 2,
|
||||
// mode: DiffOpAddMode::Append
|
||||
// },
|
||||
// ],
|
||||
// ..Default::default()
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn only_removes() {
|
||||
// let diff =
|
||||
// diff(&[1, 2, 3].into_fx_index_set(), &[3].into_fx_index_set());
|
||||
|
||||
// assert_eq!(
|
||||
// diff,
|
||||
// Diff {
|
||||
// removed: vec![DiffOpRemove { at: 0 }, DiffOpRemove { at: 1 }],
|
||||
// ..Default::default()
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn adds_with_no_move() {
|
||||
// let diff =
|
||||
// diff(&[3].into_fx_index_set(), &[1, 2, 3].into_fx_index_set());
|
||||
|
||||
// assert_eq!(
|
||||
// diff,
|
||||
// Diff {
|
||||
// added: vec![
|
||||
// DiffOpAdd {
|
||||
// at: 0,
|
||||
// ..Default::default()
|
||||
// },
|
||||
// DiffOpAdd {
|
||||
// at: 1,
|
||||
// ..Default::default()
|
||||
// },
|
||||
// ],
|
||||
// ..Default::default()
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -158,13 +158,19 @@ pub(crate) fn add_delegated_event_listener(
|
||||
}
|
||||
|
||||
// navigate up tree
|
||||
if let Some(parent) =
|
||||
node.unchecked_ref::<web_sys::Node>().parent_node()
|
||||
let host =
|
||||
js_sys::Reflect::get(&node, &JsValue::from_str("host"))
|
||||
.unwrap_throw();
|
||||
if host.is_truthy()
|
||||
&& host != node
|
||||
&& host.dyn_ref::<web_sys::Node>().is_some()
|
||||
{
|
||||
node = host;
|
||||
} else if let Some(parent) =
|
||||
node.unchecked_into::<web_sys::Node>().parent_node()
|
||||
{
|
||||
node = parent.into()
|
||||
} else if let Some(root) = node.dyn_ref::<web_sys::ShadowRoot>() {
|
||||
node = root.host().unchecked_into();
|
||||
} else {
|
||||
} else {
|
||||
node = JsValue::null()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,9 +483,6 @@ impl Text {
|
||||
|
||||
/// A leptos view which can be mounted to the DOM.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
#[must_use = "You are creating a View 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."]
|
||||
pub enum View {
|
||||
/// HTML element node.
|
||||
Element(Element),
|
||||
@@ -732,7 +729,7 @@ impl View {
|
||||
c.children.iter().cloned().for_each(|c| {
|
||||
let event_handler = event_handler.clone();
|
||||
|
||||
_ = c.on(event.clone(), Box::new(move |e| event_handler.borrow_mut()(e)));
|
||||
c.on(event.clone(), Box::new(move |e| event_handler.borrow_mut()(e)));
|
||||
});
|
||||
}
|
||||
Self::CoreComponent(c) => match c {
|
||||
@@ -841,7 +838,9 @@ where
|
||||
crate::console_warn(
|
||||
"You have both `csr` and `ssr` or `hydrate` and `ssr` enabled as \
|
||||
features, which may cause issues like <Suspense/>` failing to work \
|
||||
silently.",
|
||||
silently. `csr` is enabled by default on `leptos`, and can be \
|
||||
disabled by adding `default-features = false` to your `leptos` \
|
||||
dependency.",
|
||||
);
|
||||
|
||||
cfg_if! {
|
||||
|
||||
@@ -79,7 +79,6 @@ pub fn class_helper(
|
||||
name: Cow<'static, str>,
|
||||
value: Class,
|
||||
) {
|
||||
use crate::HydrationCtx;
|
||||
use leptos_reactive::create_render_effect;
|
||||
|
||||
let class_list = el.class_list();
|
||||
@@ -87,9 +86,7 @@ pub fn class_helper(
|
||||
Class::Fn(cx, f) => {
|
||||
create_render_effect(cx, move |old| {
|
||||
let new = f();
|
||||
if old.as_ref() != Some(&new)
|
||||
&& (old.is_some() || new || HydrationCtx::is_hydrating())
|
||||
{
|
||||
if old.as_ref() != Some(&new) && (old.is_some() || new) {
|
||||
class_expression(&class_list, &name, new, true)
|
||||
}
|
||||
new
|
||||
|
||||
@@ -368,7 +368,9 @@ impl View {
|
||||
crate::console_error(
|
||||
"\n[DANGER] You have both `csr` and `ssr` or `hydrate` and `ssr` \
|
||||
enabled as features, which may cause issues like <Suspense/>` \
|
||||
failing to work silently.\n",
|
||||
failing to work silently. `csr` is enabled by default on \
|
||||
`leptos`, and can be disabled by adding `default-features = \
|
||||
false` to your `leptos` dependency.\n",
|
||||
);
|
||||
|
||||
self.render_to_string_helper(false)
|
||||
|
||||
@@ -59,7 +59,9 @@ pub fn render_to_stream_in_order_with_prefix(
|
||||
crate::console_error(
|
||||
"\n[DANGER] You have both `csr` and `ssr` or `hydrate` and `ssr` \
|
||||
enabled as features, which may cause issues like <Suspense/>` \
|
||||
failing to work silently.\n",
|
||||
failing to work silently. `csr` is enabled by default on `leptos`, \
|
||||
and can be disabled by adding `default-features = false` to your \
|
||||
`leptos` dependency.\n",
|
||||
);
|
||||
|
||||
let (stream, runtime, _) =
|
||||
|
||||
@@ -24,4 +24,4 @@ proc-macro2 = { version = "1", features = ["span-locations", "nightly"] }
|
||||
parking_lot = "0.12"
|
||||
walkdir = "2"
|
||||
camino = "1.1.3"
|
||||
indexmap = "2"
|
||||
indexmap = "1.9.2"
|
||||
|
||||
@@ -41,7 +41,7 @@ web-sys = { version = "0.3", optional = true, features = [
|
||||
"Window",
|
||||
] }
|
||||
cfg-if = "1"
|
||||
indexmap = "2"
|
||||
indexmap = "1"
|
||||
self_cell = "1.0.0"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -863,21 +863,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
// on cleanup of this component, remove this read from parent `<Suspense/>`
|
||||
// it will be added back in when this is rendered again
|
||||
if let Some(s) = suspense_cx {
|
||||
crate::on_cleanup(cx, {
|
||||
let suspense_contexts = Rc::clone(&suspense_contexts);
|
||||
move || {
|
||||
if let Ok(ref mut contexts) =
|
||||
suspense_contexts.try_borrow_mut()
|
||||
{
|
||||
contexts.remove(&s);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let increment = move |_: Option<()>| {
|
||||
if let Some(s) = &suspense_cx {
|
||||
if let Ok(ref mut contexts) = suspense_contexts.try_borrow_mut()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.4.2"
|
||||
version = "0.4.0"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -12,7 +12,7 @@ cfg-if = "1"
|
||||
leptos = { workspace = true }
|
||||
tracing = "0.1"
|
||||
wasm-bindgen = "0.2"
|
||||
indexmap = "2"
|
||||
indexmap = "1"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.4.2"
|
||||
version = "0.4.0"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -17,7 +17,6 @@ gloo-net = { version = "0.2", features = ["http"] }
|
||||
lazy_static = "1"
|
||||
linear-map = { version = "1", features = ["serde_impl"] }
|
||||
log = "0.4"
|
||||
once_cell = "1.18"
|
||||
regex = { version = "1", optional = true }
|
||||
url = { version = "2", optional = true }
|
||||
percent-encoding = "2"
|
||||
|
||||
@@ -143,7 +143,10 @@ where
|
||||
match Url::try_from(resp_url.as_str()) {
|
||||
Ok(url) => {
|
||||
if url.origin
|
||||
!= current_window_origin()
|
||||
!= window()
|
||||
.location()
|
||||
.origin()
|
||||
.unwrap_or_default()
|
||||
{
|
||||
_ = window()
|
||||
.location()
|
||||
@@ -224,7 +227,10 @@ where
|
||||
match Url::try_from(resp_url.as_str()) {
|
||||
Ok(url) => {
|
||||
if url.origin
|
||||
!= current_window_origin()
|
||||
!= window()
|
||||
.location()
|
||||
.hostname()
|
||||
.unwrap_or_default()
|
||||
{
|
||||
_ = window()
|
||||
.location()
|
||||
@@ -322,20 +328,6 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
fn current_window_origin() -> String {
|
||||
let location = window().location();
|
||||
let protocol = location.protocol().unwrap_or_default();
|
||||
let hostname = location.hostname().unwrap_or_default();
|
||||
let port = location.port().unwrap_or_default();
|
||||
format!(
|
||||
"{}//{}{}{}",
|
||||
protocol,
|
||||
hostname,
|
||||
if port.is_empty() { "" } else { ":" },
|
||||
port
|
||||
)
|
||||
}
|
||||
|
||||
/// Automatically turns a server [Action](leptos_server::Action) into an HTML
|
||||
/// [`form`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)
|
||||
/// progressively enhanced to use client-side routing.
|
||||
@@ -425,67 +417,70 @@ where
|
||||
let on_response = Rc::new(move |resp: &web_sys::Response| {
|
||||
let resp = resp.clone().expect("couldn't get Response");
|
||||
spawn_local(async move {
|
||||
let body = JsFuture::from(
|
||||
resp.text().expect("couldn't get .text() from Response"),
|
||||
)
|
||||
.await;
|
||||
let status = resp.status();
|
||||
match body {
|
||||
Ok(json) => {
|
||||
let json = json
|
||||
.as_string()
|
||||
.expect("couldn't get String from JsString");
|
||||
if (500..=599).contains(&status) {
|
||||
match serde_json::from_str::<ServerFnError>(&json) {
|
||||
Ok(res) => {
|
||||
value.try_set(Some(Err(res)));
|
||||
if let Some(error) = error {
|
||||
error.try_set(None);
|
||||
let redirected = resp.redirected();
|
||||
if !redirected {
|
||||
let body = JsFuture::from(
|
||||
resp.text().expect("couldn't get .text() from Response"),
|
||||
)
|
||||
.await;
|
||||
let status = resp.status();
|
||||
match body {
|
||||
Ok(json) => {
|
||||
let json = json
|
||||
.as_string()
|
||||
.expect("couldn't get String from JsString");
|
||||
if (500..=599).contains(&status) {
|
||||
match serde_json::from_str::<ServerFnError>(&json) {
|
||||
Ok(res) => {
|
||||
value.try_set(Some(Err(res)));
|
||||
if let Some(error) = error {
|
||||
error.try_set(None);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
value.try_set(Some(Err(
|
||||
ServerFnError::Deserialization(
|
||||
e.to_string(),
|
||||
),
|
||||
)));
|
||||
if let Some(error) = error {
|
||||
error.try_set(Some(Box::new(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
value.try_set(Some(Err(
|
||||
ServerFnError::Deserialization(
|
||||
e.to_string(),
|
||||
),
|
||||
)));
|
||||
if let Some(error) = error {
|
||||
error.try_set(Some(Box::new(e)));
|
||||
} else {
|
||||
match serde_json::from_str::<O>(&json) {
|
||||
Ok(res) => {
|
||||
value.try_set(Some(Ok(res)));
|
||||
if let Some(error) = error {
|
||||
error.try_set(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match serde_json::from_str::<O>(&json) {
|
||||
Ok(res) => {
|
||||
value.try_set(Some(Ok(res)));
|
||||
if let Some(error) = error {
|
||||
error.try_set(None);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
value.try_set(Some(Err(
|
||||
ServerFnError::Deserialization(
|
||||
e.to_string(),
|
||||
),
|
||||
)));
|
||||
if let Some(error) = error {
|
||||
error.try_set(Some(Box::new(e)));
|
||||
Err(e) => {
|
||||
value.try_set(Some(Err(
|
||||
ServerFnError::Deserialization(
|
||||
e.to_string(),
|
||||
),
|
||||
)));
|
||||
if let Some(error) = error {
|
||||
error.try_set(Some(Box::new(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{e:?}");
|
||||
if let Some(error) = error {
|
||||
error.try_set(Some(Box::new(
|
||||
ServerFnErrorErr::Request(
|
||||
e.as_string().unwrap_or_default(),
|
||||
),
|
||||
)));
|
||||
Err(e) => {
|
||||
error!("{e:?}");
|
||||
if let Some(error) = error {
|
||||
error.try_set(Some(Box::new(
|
||||
ServerFnErrorErr::Request(
|
||||
e.as_string().unwrap_or_default(),
|
||||
),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
cx.batch(move || {
|
||||
input.try_set(None);
|
||||
action.set_pending(false);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::{use_location, use_resolved_path, State};
|
||||
use leptos::{leptos_dom::IntoView, *};
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Describes a value that is either a static or a reactive URL, i.e.,
|
||||
/// a [String], a [&str], or a reactive `Fn() -> String`.
|
||||
@@ -57,16 +56,6 @@ pub fn A<H>(
|
||||
/// if false, link is marked active if the current route starts with it.
|
||||
#[prop(optional)]
|
||||
exact: bool,
|
||||
/// Provides a class to be added when the link is active. If provided, it will
|
||||
/// be added at the same time that the `aria-current` attribute is set.
|
||||
///
|
||||
/// This supports multiple space-separated class names.
|
||||
///
|
||||
/// **Performance**: If it’s possible to style the link using the CSS with the
|
||||
/// `[aria-current=page]` selector, you should prefer that, as it enables significant
|
||||
/// SSR optimizations.
|
||||
#[prop(optional, into)]
|
||||
active_class: Option<Cow<'static, str>>,
|
||||
/// An object of any type that will be pushed to router state
|
||||
#[prop(optional)]
|
||||
state: Option<State>,
|
||||
@@ -94,13 +83,12 @@ where
|
||||
cx: Scope,
|
||||
href: Memo<Option<String>>,
|
||||
exact: bool,
|
||||
#[allow(unused)] state: Option<State>,
|
||||
#[allow(unused)] replace: bool,
|
||||
state: Option<State>,
|
||||
replace: bool,
|
||||
class: Option<AttributeValue>,
|
||||
#[allow(unused)] active_class: Option<Cow<'static, str>>,
|
||||
id: Option<String>,
|
||||
children: Children,
|
||||
) -> View {
|
||||
) -> HtmlElement<leptos::html::A> {
|
||||
#[cfg(not(any(feature = "hydrate", feature = "csr")))]
|
||||
{
|
||||
_ = state;
|
||||
@@ -130,86 +118,20 @@ where
|
||||
}
|
||||
});
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
// if we have `active_class`, the SSR optimization doesn't play nicely
|
||||
// so we use the builder instead
|
||||
if let Some(active_class) = active_class {
|
||||
let mut a = leptos::html::a(cx)
|
||||
.attr("href", move || href.get().unwrap_or_default())
|
||||
.attr("aria-current", move || {
|
||||
if is_active.get() {
|
||||
Some("page")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.attr(
|
||||
"class",
|
||||
class.map(|class| class.into_attribute_boxed(cx)),
|
||||
);
|
||||
|
||||
for class_name in active_class.split_ascii_whitespace() {
|
||||
a = a.class(class_name.to_string(), move || is_active.get())
|
||||
}
|
||||
|
||||
a.attr("id", id).child(children(cx)).into_view(cx)
|
||||
}
|
||||
// but keep the nice SSR optimization in most cases
|
||||
else {
|
||||
view! { cx,
|
||||
<a
|
||||
href=move || href.get().unwrap_or_default()
|
||||
aria-current=move || if is_active.get() { Some("page") } else { None }
|
||||
class=class
|
||||
id=id
|
||||
>
|
||||
{children(cx)}
|
||||
</a>
|
||||
}
|
||||
.into_view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
// the non-SSR version doesn't need the SSR optimizations
|
||||
// DRY here to avoid WASM binary size bloat
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
{
|
||||
let a = view! { cx,
|
||||
<a
|
||||
href=move || href.get().unwrap_or_default()
|
||||
prop:state={state.map(|s| s.to_js_value())}
|
||||
prop:replace={replace}
|
||||
aria-current=move || if is_active.get() { Some("page") } else { None }
|
||||
class=class
|
||||
id=id
|
||||
>
|
||||
{children(cx)}
|
||||
</a>
|
||||
};
|
||||
if let Some(active_class) = active_class {
|
||||
let mut a = a;
|
||||
for class_name in active_class.split_ascii_whitespace() {
|
||||
a = a.class(class_name.to_string(), move || is_active.get())
|
||||
}
|
||||
a
|
||||
} else {
|
||||
a
|
||||
}
|
||||
.into_view(cx)
|
||||
view! { cx,
|
||||
<a
|
||||
href=move || href.get().unwrap_or_default()
|
||||
prop:state={state.map(|s| s.to_js_value())}
|
||||
prop:replace={replace}
|
||||
aria-current=move || if is_active.get() { Some("page") } else { None }
|
||||
class=class
|
||||
id=id
|
||||
>
|
||||
{children(cx)}
|
||||
</a>
|
||||
}
|
||||
}
|
||||
|
||||
let href = use_resolved_path(cx, move || href.to_href()());
|
||||
inner(
|
||||
cx,
|
||||
href,
|
||||
exact,
|
||||
state,
|
||||
replace,
|
||||
class,
|
||||
active_class,
|
||||
id,
|
||||
children,
|
||||
)
|
||||
inner(cx, href, exact, state, replace, class, id, children)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ where
|
||||
{
|
||||
// resolve relative path
|
||||
let path = use_resolved_path(cx, move || path.to_string());
|
||||
let path = path.get_untracked().unwrap_or_else(|| "/".to_string());
|
||||
let path = path.get().unwrap_or_else(|| "/".to_string());
|
||||
|
||||
// redirect on the server
|
||||
if let Some(redirect_fn) = use_context::<ServerRedirectFunction>(cx) {
|
||||
|
||||
@@ -126,16 +126,12 @@ impl History for BrowserIntegration {
|
||||
.unwrap_or(hash);
|
||||
let el = leptos_dom::document().get_element_by_id(&hash);
|
||||
if let Some(el) = el {
|
||||
el.scroll_into_view();
|
||||
return;
|
||||
el.scroll_into_view()
|
||||
} else if loc.scroll {
|
||||
leptos_dom::window().scroll_to_with_x_and_y(0.0, 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// scroll to top
|
||||
if loc.scroll {
|
||||
leptos_dom::window().scroll_to_with_x_and_y(0.0, 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,20 +3,14 @@ use std::borrow::Cow;
|
||||
#[doc(hidden)]
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn expand_optionals(pattern: &str) -> Vec<Cow<str>> {
|
||||
use js_sys::RegExp;
|
||||
use once_cell::unsync::Lazy;
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
thread_local! {
|
||||
static OPTIONAL_RE: Lazy<RegExp> = Lazy::new(|| {
|
||||
RegExp::new(OPTIONAL, "")
|
||||
});
|
||||
static OPTIONAL_RE_2: Lazy<RegExp> = Lazy::new(|| {
|
||||
RegExp::new(OPTIONAL_2, "")
|
||||
});
|
||||
}
|
||||
#[allow(non_snake_case)]
|
||||
let OPTIONAL_RE = js_sys::RegExp::new(OPTIONAL, "");
|
||||
#[allow(non_snake_case)]
|
||||
let OPTIONAL_RE_2 = js_sys::RegExp::new(OPTIONAL_2, "");
|
||||
|
||||
let captures = OPTIONAL_RE.with(|re| re.exec(pattern));
|
||||
let captures = OPTIONAL_RE.exec(pattern);
|
||||
match captures {
|
||||
None => vec![pattern.into()],
|
||||
Some(matched) => {
|
||||
@@ -34,7 +28,7 @@ pub fn expand_optionals(pattern: &str) -> Vec<Cow<str>> {
|
||||
prefixes.push(prefix.clone());
|
||||
|
||||
while let Some(matched) =
|
||||
OPTIONAL_RE_2.with(|re| re.exec(suffix.trim_start_matches('?')))
|
||||
OPTIONAL_RE_2.exec(suffix.trim_start_matches('?'))
|
||||
{
|
||||
prefix += &matched.get(1).as_string().unwrap();
|
||||
prefixes.push(prefix.clone());
|
||||
|
||||
Reference in New Issue
Block a user