mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 21:53:07 -05:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab24ee395a | ||
|
|
ff576f8f47 |
@@ -1,4 +1,4 @@
|
||||
name: CI Examples
|
||||
name: Check Examples
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -11,10 +11,12 @@ on:
|
||||
jobs:
|
||||
get-leptos-changed:
|
||||
uses: ./.github/workflows/get-leptos-changed.yml
|
||||
|
||||
get-examples-matrix:
|
||||
uses: ./.github/workflows/get-examples-matrix.yml
|
||||
|
||||
test:
|
||||
name: CI
|
||||
name: Check
|
||||
needs: [get-leptos-changed, get-examples-matrix]
|
||||
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
|
||||
strategy:
|
||||
@@ -23,5 +25,5 @@ jobs:
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
cargo_make_task: "check"
|
||||
toolchain: nightly
|
||||
@@ -1,4 +1,4 @@
|
||||
name: CI Stable Examples
|
||||
name: Check stable
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
uses: ./.github/workflows/get-leptos-changed.yml
|
||||
|
||||
test:
|
||||
name: CI
|
||||
name: Check
|
||||
needs: [get-leptos-changed]
|
||||
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
|
||||
strategy:
|
||||
@@ -22,5 +22,5 @@ jobs:
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
cargo_make_task: "check"
|
||||
toolchain: stable
|
||||
26
.github/workflows/verify-all-examples.yml
vendored
Normal file
26
.github/workflows/verify-all-examples.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: CI Examples
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
schedule:
|
||||
# Run once a day at 3:00 AM EST
|
||||
- cron: "0 8 * * *"
|
||||
|
||||
jobs:
|
||||
get-examples-matrix:
|
||||
uses: ./.github/workflows/get-examples-matrix.yml
|
||||
|
||||
test:
|
||||
name: CI
|
||||
needs: [get-examples-matrix]
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.get-examples-matrix.outputs.matrix) }}
|
||||
fail-fast: false
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly
|
||||
28
Cargo.toml
28
Cargo.toml
@@ -26,22 +26,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.5.3"
|
||||
version = "0.5.2"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", version = "0.5.3" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.5.3" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.3" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.5.3" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.5.3" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.5.3" }
|
||||
server_fn = { path = "./server_fn", version = "0.5.3" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.5.3" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.3" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.5.3" }
|
||||
leptos_router = { path = "./router", version = "0.5.3" }
|
||||
leptos_meta = { path = "./meta", version = "0.5.3" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.3" }
|
||||
leptos = { path = "./leptos", version = "0.5.2" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.5.2" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.2" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.5.2" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.5.2" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.5.2" }
|
||||
server_fn = { path = "./server_fn", version = "0.5.2" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.5.2" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.2" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.5.2" }
|
||||
leptos_router = { path = "./router", version = "0.5.2" }
|
||||
leptos_meta = { path = "./meta", version = "0.5.2" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.2" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -40,8 +40,6 @@ Example projects depend on the following tools. Please install them as needed.
|
||||
- [Cargo Make](https://sagiegurari.github.io/cargo-make/)
|
||||
- Run `cargo install --force cargo-make`
|
||||
- Setup a command alias like `alias cm='cargo make'` to reduce typing (**_Optional_**)
|
||||
- [Trunk](https://github.com/thedodd/trunk)
|
||||
- Run `cargo install trunk`
|
||||
- [Node Version Manager](https://github.com/nvm-sh/nvm/) (**_Optional_**)
|
||||
- [Node.js](https://nodejs.org/)
|
||||
- [pnpm](https://pnpm.io/) (**_Optional_**)
|
||||
|
||||
@@ -89,15 +89,15 @@ pub fn fetch_example() -> impl IntoView {
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<Transition fallback=move || {
|
||||
view! { <div>"Loading (Suspense Fallback)..."</div> }
|
||||
}>
|
||||
<ErrorBoundary fallback>
|
||||
<ErrorBoundary fallback>
|
||||
<Transition fallback=move || {
|
||||
view! { <div>"Loading (Suspense Fallback)..."</div> }
|
||||
}>
|
||||
<div>
|
||||
{cats_view}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</Transition>
|
||||
</Transition>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,5 +12,4 @@ console_error_panic_hook = "0.1.7"
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
wasm-bindgen = "0.2"
|
||||
web-sys = "0.3"
|
||||
gloo-timers = { version = "0.3", features = ["futures"] }
|
||||
web-sys = "0.3"
|
||||
@@ -6,12 +6,8 @@ use leptos::*;
|
||||
use portal::App;
|
||||
use web_sys::HtmlButtonElement;
|
||||
|
||||
async fn next_tick() {
|
||||
gloo_timers::future::TimeoutFuture::new(25).await;
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn portal() {
|
||||
fn portal() {
|
||||
let document = leptos::document();
|
||||
let body = document.body().unwrap();
|
||||
|
||||
@@ -28,7 +24,7 @@ async fn portal() {
|
||||
|
||||
show_button.click();
|
||||
|
||||
next_tick().await;
|
||||
// next_tick().await;
|
||||
|
||||
// check HTML
|
||||
assert_eq!(
|
||||
|
||||
@@ -25,16 +25,16 @@ tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.4", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.22.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.8" }
|
||||
sqlx = { version = "0.7.2", features = [
|
||||
sqlx = { version = "0.6.2", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
thiserror = "1.0.38"
|
||||
wasm-bindgen = "0.2"
|
||||
axum_session_auth = { version = "0.9.0", features = [
|
||||
axum_session_auth = { version = "0.2.1", features = [
|
||||
"sqlite-rustls",
|
||||
], optional = true }
|
||||
axum_session = { version = "0.9.0", features = [
|
||||
axum_session = { version = "0.2.3", features = [
|
||||
"sqlite-rustls",
|
||||
], optional = true }
|
||||
bcrypt = { version = "0.14", optional = true }
|
||||
|
||||
Binary file not shown.
@@ -56,7 +56,8 @@ if #[cfg(feature = "ssr")] {
|
||||
// Auth section
|
||||
let session_config = SessionConfig::default().with_table_name("axum_sessions");
|
||||
let auth_config = AuthConfig::<i64>::default();
|
||||
let session_store = SessionStore::<SessionSqlitePool>::new(Some(pool.clone().into()), session_config).await.unwrap();
|
||||
let session_store = SessionStore::<SessionSqlitePool>::new(Some(pool.clone().into()), session_config);
|
||||
session_store.initiate().await.unwrap();
|
||||
|
||||
sqlx::migrate!()
|
||||
.run(&pool)
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
[package]
|
||||
name = "todo_app_sqlite_csr"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
leptos_integration_utils = { path = "../../integrations/utils", optional = true }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
axum = { version = "0.6.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.4", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.22.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.8" }
|
||||
sqlx = { version = "0.6.2", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
thiserror = "1.0.38"
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[features]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:tokio",
|
||||
"dep:sqlx",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"dep:leptos_axum",
|
||||
"dep:leptos_integration_utils",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "todo_app_sqlite_csr"
|
||||
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||
site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "./style.css"
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
assets-dir = "public"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
end2end-cmd = "cargo make test-ui"
|
||||
end2end-dir = "e2e"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
# The features to use when compiling the bin target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --bin-features
|
||||
bin-features = ["ssr"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the bin target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
bin-default-features = false
|
||||
|
||||
# The features to use when compiling the lib target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --lib-features
|
||||
lib-features = ["csr"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the lib target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
lib-default-features = false
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Greg Johnston
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,12 +0,0 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos-webdriver-test.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
CLIENT_PROCESS_NAME = "todo_app_sqlite_csr"
|
||||
|
||||
[tasks.test-ui]
|
||||
cwd = "./e2e"
|
||||
command = "cargo"
|
||||
args = ["make", "test-ui", "${@}"]
|
||||
@@ -1,15 +0,0 @@
|
||||
# Leptos Todo App Sqlite with CSR
|
||||
|
||||
This example shows how to combine client-side rendering with server functions, i.e., using server functions as a convenient way to create an ad hoc API, but without using server-side rendering and hydration.
|
||||
|
||||
## Getting Started
|
||||
|
||||
See the [Examples README](../README.md) for setup and run instructions.
|
||||
|
||||
## E2E Testing
|
||||
|
||||
See the [E2E README](./e2e/README.md) for more information about the testing strategy.
|
||||
|
||||
## Rendering
|
||||
|
||||
See the [SSR Notes](../SSR_NOTES.md) for more information about Server Side Rendering.
|
||||
Binary file not shown.
@@ -1,18 +0,0 @@
|
||||
[package]
|
||||
name = "todo_app_sqlite_csr_e2e"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0.72"
|
||||
async-trait = "0.1.72"
|
||||
cucumber = "0.19.1"
|
||||
fantoccini = "0.19.3"
|
||||
pretty_assertions = "1.4.0"
|
||||
serde_json = "1.0.104"
|
||||
tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread", "time"] }
|
||||
url = "2.4.0"
|
||||
|
||||
[[test]]
|
||||
name = "app_suite"
|
||||
harness = false # Allow Cucumber to print output instead of libtest
|
||||
@@ -1,20 +0,0 @@
|
||||
extend = { path = "../../cargo-make/main.toml" }
|
||||
|
||||
[tasks.test]
|
||||
env = { RUN_AUTOMATICALLY = false }
|
||||
condition = { env_true = ["RUN_AUTOMATICALLY"] }
|
||||
|
||||
[tasks.ci]
|
||||
|
||||
[tasks.test-ui]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"test",
|
||||
"--test",
|
||||
"app_suite",
|
||||
"--",
|
||||
"--retry",
|
||||
"5",
|
||||
"--fail-fast",
|
||||
"${@}",
|
||||
]
|
||||
@@ -1,34 +0,0 @@
|
||||
# E2E Testing
|
||||
|
||||
This example demonstrates e2e testing with Rust using executable requirements.
|
||||
|
||||
## Testing Stack
|
||||
|
||||
| | Role | Description |
|
||||
|---|---|---|
|
||||
| [Cucumber](https://github.com/cucumber-rs/cucumber/tree/main) | Test Runner | Run [Gherkin](https://cucumber.io/docs/gherkin/reference/) specifications as Rust tests |
|
||||
| [Fantoccini](https://github.com/jonhoo/fantoccini/tree/main) | Browser Client | Interact with web pages through WebDriver |
|
||||
| [Cargo Leptos ](https://github.com/leptos-rs/cargo-leptos) | Build Tool | Compile example and start the server and end-2-end tests |
|
||||
| [chromedriver](https://chromedriver.chromium.org/downloads) | WebDriver | Provide WebDriver for Chrome
|
||||
|
||||
## Testing Organization
|
||||
|
||||
Testing is organized around what a user can do and see/not see. Test scenarios are grouped by the **user action** and the **object** of that action. This makes it easier to locate and reason about requirements.
|
||||
|
||||
Here is a brief overview of how things fit together.
|
||||
|
||||
```bash
|
||||
features
|
||||
└── {action}_{object}.feature # Specify test scenarios
|
||||
tests
|
||||
├── fixtures
|
||||
│ ├── action.rs # Perform a user action (click, type, etc.)
|
||||
│ ├── check.rs # Assert what a user can see/not see
|
||||
│ ├── find.rs # Query page elements
|
||||
│ ├── mod.rs
|
||||
│ └── world
|
||||
│ ├── action_steps.rs # Map Gherkin steps to user actions
|
||||
│ ├── check_steps.rs # Map Gherkin steps to user expectations
|
||||
│ └── mod.rs
|
||||
└── app_suite.rs # Test main
|
||||
```
|
||||
@@ -1,16 +0,0 @@
|
||||
@add_todo
|
||||
Feature: Add Todo
|
||||
|
||||
Background:
|
||||
Given I see the app
|
||||
|
||||
@add_todo-see
|
||||
Scenario: Should see the todo
|
||||
Given I set the todo as Buy Bread
|
||||
When I click the Add button
|
||||
Then I see the todo named Buy Bread
|
||||
|
||||
@add_todo-style
|
||||
Scenario: Should see the pending todo
|
||||
When I add a todo as Buy Oranges
|
||||
Then I see the pending todo
|
||||
@@ -1,18 +0,0 @@
|
||||
@delete_todo
|
||||
Feature: Delete Todo
|
||||
|
||||
Background:
|
||||
Given I see the app
|
||||
|
||||
@serial
|
||||
@delete_todo-remove
|
||||
Scenario: Should not see the deleted todo
|
||||
Given I add a todo as Buy Yogurt
|
||||
When I delete the todo named Buy Yogurt
|
||||
Then I do not see the todo named Buy Yogurt
|
||||
|
||||
@serial
|
||||
@delete_todo-message
|
||||
Scenario: Should see the empty list message
|
||||
When I empty the todo list
|
||||
Then I see the empty list message is No tasks were found.
|
||||
@@ -1,12 +0,0 @@
|
||||
@open_app
|
||||
Feature: Open App
|
||||
|
||||
@open_app-title
|
||||
Scenario: Should see the home page title
|
||||
When I open the app
|
||||
Then I see the page title is My Tasks
|
||||
|
||||
@open_app-label
|
||||
Scenario: Should see the input label
|
||||
When I open the app
|
||||
Then I see the label of the input is Add a Todo
|
||||
@@ -1,14 +0,0 @@
|
||||
mod fixtures;
|
||||
|
||||
use anyhow::Result;
|
||||
use cucumber::World;
|
||||
use fixtures::world::AppWorld;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
AppWorld::cucumber()
|
||||
.fail_on_skipped()
|
||||
.run_and_exit("./features")
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
use super::{find, world::HOST};
|
||||
use anyhow::Result;
|
||||
use fantoccini::Client;
|
||||
use std::result::Result::Ok;
|
||||
use tokio::{self, time};
|
||||
|
||||
pub async fn goto_path(client: &Client, path: &str) -> Result<()> {
|
||||
let url = format!("{}{}", HOST, path);
|
||||
client.goto(&url).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_todo(client: &Client, text: &str) -> Result<()> {
|
||||
fill_todo(client, text).await?;
|
||||
click_add_button(client).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn fill_todo(client: &Client, text: &str) -> Result<()> {
|
||||
let textbox = find::todo_input(client).await;
|
||||
textbox.send_keys(text).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn click_add_button(client: &Client) -> Result<()> {
|
||||
let add_button = find::add_button(client).await;
|
||||
add_button.click().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn empty_todo_list(client: &Client) -> Result<()> {
|
||||
let todos = find::todos(client).await;
|
||||
|
||||
for _todo in todos {
|
||||
let _ = delete_first_todo(client).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_first_todo(client: &Client) -> Result<()> {
|
||||
if let Some(element) = find::first_delete_button(client).await {
|
||||
element.click().await.expect("Failed to delete todo");
|
||||
time::sleep(time::Duration::from_millis(250)).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_todo(client: &Client, text: &str) -> Result<()> {
|
||||
if let Some(element) = find::delete_button(client, text).await {
|
||||
element.click().await?;
|
||||
time::sleep(time::Duration::from_millis(250)).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
use super::find;
|
||||
use anyhow::{Ok, Result};
|
||||
use fantoccini::{Client, Locator};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
pub async fn text_on_element(
|
||||
client: &Client,
|
||||
selector: &str,
|
||||
expected_text: &str,
|
||||
) -> Result<()> {
|
||||
let element = client
|
||||
.wait()
|
||||
.for_element(Locator::Css(selector))
|
||||
.await
|
||||
.expect(
|
||||
format!("Element not found by Css selector `{}`", selector)
|
||||
.as_str(),
|
||||
);
|
||||
|
||||
let actual = element.text().await?;
|
||||
assert_eq!(&actual, expected_text);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn todo_present(
|
||||
client: &Client,
|
||||
text: &str,
|
||||
expected: bool,
|
||||
) -> Result<()> {
|
||||
let todo_present = is_todo_present(client, text).await;
|
||||
|
||||
assert_eq!(todo_present, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn is_todo_present(client: &Client, text: &str) -> bool {
|
||||
let todos = find::todos(client).await;
|
||||
|
||||
for todo in todos {
|
||||
let todo_title = todo.text().await.expect("Todo title not found");
|
||||
if todo_title == text {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub async fn todo_is_pending(client: &Client) -> Result<()> {
|
||||
if let None = find::pending_todo(client).await {
|
||||
assert!(false, "Pending todo not found");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
use fantoccini::{elements::Element, Client, Locator};
|
||||
|
||||
pub async fn todo_input(client: &Client) -> Element {
|
||||
let textbox = client
|
||||
.wait()
|
||||
.for_element(Locator::Css("input[name='title"))
|
||||
.await
|
||||
.expect("Todo textbox not found");
|
||||
|
||||
textbox
|
||||
}
|
||||
|
||||
pub async fn add_button(client: &Client) -> Element {
|
||||
let button = client
|
||||
.wait()
|
||||
.for_element(Locator::Css("input[value='Add']"))
|
||||
.await
|
||||
.expect("");
|
||||
|
||||
button
|
||||
}
|
||||
|
||||
pub async fn first_delete_button(client: &Client) -> Option<Element> {
|
||||
if let Ok(element) = client
|
||||
.wait()
|
||||
.for_element(Locator::Css("li:first-child input[value='X']"))
|
||||
.await
|
||||
{
|
||||
return Some(element);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn delete_button(client: &Client, text: &str) -> Option<Element> {
|
||||
let selector = format!("//*[text()='{text}']//input[@value='X']");
|
||||
if let Ok(element) =
|
||||
client.wait().for_element(Locator::XPath(&selector)).await
|
||||
{
|
||||
return Some(element);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn pending_todo(client: &Client) -> Option<Element> {
|
||||
if let Ok(element) =
|
||||
client.wait().for_element(Locator::Css(".pending")).await
|
||||
{
|
||||
return Some(element);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn todos(client: &Client) -> Vec<Element> {
|
||||
let todos = client
|
||||
.find_all(Locator::Css("li"))
|
||||
.await
|
||||
.expect("Todo List not found");
|
||||
|
||||
todos
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
pub mod action;
|
||||
pub mod check;
|
||||
pub mod find;
|
||||
pub mod world;
|
||||
@@ -1,57 +0,0 @@
|
||||
use crate::fixtures::{action, world::AppWorld};
|
||||
use anyhow::{Ok, Result};
|
||||
use cucumber::{given, when};
|
||||
|
||||
#[given("I see the app")]
|
||||
#[when("I open the app")]
|
||||
async fn i_open_the_app(world: &mut AppWorld) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::goto_path(client, "").await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[given(regex = "^I add a todo as (.*)$")]
|
||||
#[when(regex = "^I add a todo as (.*)$")]
|
||||
async fn i_add_a_todo_titled(world: &mut AppWorld, text: String) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::add_todo(client, text.as_str()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[given(regex = "^I set the todo as (.*)$")]
|
||||
async fn i_set_the_todo_as(world: &mut AppWorld, text: String) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::fill_todo(client, &text).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[when(regex = "I click the Add button$")]
|
||||
async fn i_click_the_button(world: &mut AppWorld) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::click_add_button(client).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[when(regex = "^I delete the todo named (.*)$")]
|
||||
async fn i_delete_the_todo_named(
|
||||
world: &mut AppWorld,
|
||||
text: String,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::delete_todo(client, text.as_str()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[given("the todo list is empty")]
|
||||
#[when("I empty the todo list")]
|
||||
async fn i_empty_the_todo_list(world: &mut AppWorld) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::empty_todo_list(client).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
use crate::fixtures::{check, world::AppWorld};
|
||||
use anyhow::{Ok, Result};
|
||||
use cucumber::then;
|
||||
|
||||
#[then(regex = "^I see the page title is (.*)$")]
|
||||
async fn i_see_the_page_title_is(
|
||||
world: &mut AppWorld,
|
||||
text: String,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
check::text_on_element(client, "h1", &text).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[then(regex = "^I see the label of the input is (.*)$")]
|
||||
async fn i_see_the_label_of_the_input_is(
|
||||
world: &mut AppWorld,
|
||||
text: String,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
check::text_on_element(client, "label", &text).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[then(regex = "^I see the todo named (.*)$")]
|
||||
async fn i_see_the_todo_is_present(
|
||||
world: &mut AppWorld,
|
||||
text: String,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
check::todo_present(client, text.as_str(), true).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[then("I see the pending todo")]
|
||||
async fn i_see_the_pending_todo(world: &mut AppWorld) -> Result<()> {
|
||||
let client = &world.client;
|
||||
|
||||
check::todo_is_pending(client).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[then(regex = "^I see the empty list message is (.*)$")]
|
||||
async fn i_see_the_empty_list_message_is(
|
||||
world: &mut AppWorld,
|
||||
text: String,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
check::text_on_element(client, "ul p", &text).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[then(regex = "^I do not see the todo named (.*)$")]
|
||||
async fn i_do_not_see_the_todo_is_present(
|
||||
world: &mut AppWorld,
|
||||
text: String,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
check::todo_present(client, text.as_str(), false).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
pub mod action_steps;
|
||||
pub mod check_steps;
|
||||
|
||||
use anyhow::Result;
|
||||
use cucumber::World;
|
||||
use fantoccini::{
|
||||
error::NewSessionError, wd::Capabilities, Client, ClientBuilder,
|
||||
};
|
||||
|
||||
pub const HOST: &str = "http://127.0.0.1:3000";
|
||||
|
||||
#[derive(Debug, World)]
|
||||
#[world(init = Self::new)]
|
||||
pub struct AppWorld {
|
||||
pub client: Client,
|
||||
}
|
||||
|
||||
impl AppWorld {
|
||||
async fn new() -> Result<Self, anyhow::Error> {
|
||||
let webdriver_client = build_client().await?;
|
||||
|
||||
Ok(Self {
|
||||
client: webdriver_client,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_client() -> Result<Client, NewSessionError> {
|
||||
let mut cap = Capabilities::new();
|
||||
let arg = serde_json::from_str("{\"args\": [\"-headless\"]}").unwrap();
|
||||
cap.insert("goog:chromeOptions".to_string(), arg);
|
||||
|
||||
let client = ClientBuilder::native()
|
||||
.capabilities(cap)
|
||||
.connect("http://localhost:4444")
|
||||
.await?;
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
|
||||
CREATE TABLE IF NOT EXISTS todos
|
||||
(
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
title VARCHAR,
|
||||
completed BOOLEAN
|
||||
);
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,58 +0,0 @@
|
||||
use crate::errors::TodoAppError;
|
||||
use leptos::{Errors, *};
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos_axum::ResponseOptions;
|
||||
|
||||
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
|
||||
// here than just displaying them
|
||||
#[component]
|
||||
pub fn ErrorTemplate(
|
||||
#[prop(optional)] outside_errors: Option<Errors>,
|
||||
#[prop(optional)] errors: Option<RwSignal<Errors>>,
|
||||
) -> impl IntoView {
|
||||
let errors = match outside_errors {
|
||||
Some(e) => create_rw_signal(e),
|
||||
None => match errors {
|
||||
Some(e) => e,
|
||||
None => panic!("No Errors found and we expected errors!"),
|
||||
},
|
||||
};
|
||||
|
||||
// Get Errors from Signal
|
||||
// Downcast lets us take a type that implements `std::error::Error`
|
||||
let errors: Vec<TodoAppError> = errors
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter_map(|(_, v)| v.downcast_ref::<TodoAppError>().cloned())
|
||||
.collect();
|
||||
|
||||
// Only the response code for the first error is actually sent from the server
|
||||
// this may be customized by the specific application
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
let response = use_context::<ResponseOptions>();
|
||||
if let Some(response) = response {
|
||||
response.set_status(errors[0].status_code());
|
||||
}
|
||||
}
|
||||
|
||||
view! {
|
||||
<h1>"Errors"</h1>
|
||||
<For
|
||||
// a function that returns the items we're iterating over; a signal is fine
|
||||
each= move || {errors.clone().into_iter().enumerate()}
|
||||
// a unique key for each item as a reference
|
||||
key=|(index, _error)| *index
|
||||
// renders each item to a view
|
||||
children=move |error| {
|
||||
let error_string = error.1.to_string();
|
||||
let error_code= error.1.status_code();
|
||||
view! {
|
||||
|
||||
<h2>{error_code.to_string()}</h2>
|
||||
<p>"Error: " {error_string}</p>
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
use http::status::StatusCode;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum TodoAppError {
|
||||
#[error("Not Found")]
|
||||
NotFound,
|
||||
#[error("Internal Server Error")]
|
||||
InternalServerError,
|
||||
}
|
||||
|
||||
impl TodoAppError {
|
||||
pub fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
TodoAppError::NotFound => StatusCode::NOT_FOUND,
|
||||
TodoAppError::InternalServerError => {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::State,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
response::{Html, IntoResponse, Response as AxumResponse},
|
||||
};
|
||||
use leptos::LeptosOptions;
|
||||
use leptos_integration_utils::html_parts_separated;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
pub async fn file_or_index_handler(
|
||||
uri: Uri,
|
||||
State(options): State<LeptosOptions>,
|
||||
) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
let (head, tail) = html_parts_separated(&options, None);
|
||||
|
||||
Html(format!("{head}</head><body>{tail}")).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(
|
||||
uri: Uri,
|
||||
root: &str,
|
||||
) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let req = Request::builder()
|
||||
.uri(uri.clone())
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {err}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
pub mod error_template;
|
||||
pub mod errors;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod fallback;
|
||||
pub mod todo;
|
||||
|
||||
#[cfg_attr(feature = "csr", wasm_bindgen::prelude::wasm_bindgen)]
|
||||
pub fn hydrate() {
|
||||
use crate::todo::*;
|
||||
|
||||
_ = console_log::init_with_level(log::Level::Error);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::mount_to_body(TodoApp);
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
#[allow(unused)]
|
||||
mod ssr_imports {
|
||||
pub use axum::{
|
||||
body::Body as AxumBody,
|
||||
extract::{Path, State},
|
||||
http::Request,
|
||||
response::{Html, IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
pub use leptos::*;
|
||||
pub use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
pub use todo_app_sqlite_csr::{
|
||||
fallback::file_or_index_handler, todo::*, *,
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[cfg_attr(feature = "ssr", tokio::main)]
|
||||
async fn main() {
|
||||
use ssr_imports::*;
|
||||
simple_logger::init_with_level(log::Level::Error)
|
||||
.expect("couldn't initialize logging");
|
||||
|
||||
let _conn = db().await.expect("couldn't connect to DB");
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
.fallback(file_or_index_handler)
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
logging::log!("listening on http://{}", &addr);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn main() {
|
||||
// This example cannot be built as a trunk standalone CSR-only app.
|
||||
// Only the server may directly connect to the database.
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
use crate::error_template::ErrorTemplate;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
|
||||
pub struct Todo {
|
||||
id: u16,
|
||||
title: String,
|
||||
completed: bool,
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use sqlx::{Connection, SqliteConnection};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
|
||||
Ok(SqliteConnection::connect("sqlite:Todos.db").await?)
|
||||
}
|
||||
|
||||
#[server(GetTodos, "/api")]
|
||||
pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> {
|
||||
// this is just an example of how to access server context injected in the handlers
|
||||
// http::Request doesn't implement Clone, so more work will be needed to do use_context() on this
|
||||
let req_parts = use_context::<leptos_axum::RequestParts>();
|
||||
|
||||
if let Some(req_parts) = req_parts {
|
||||
println!("Uri = {:?}", req_parts.uri);
|
||||
}
|
||||
|
||||
use futures::TryStreamExt;
|
||||
|
||||
let mut conn = db().await?;
|
||||
|
||||
let mut todos = Vec::new();
|
||||
let mut rows =
|
||||
sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
|
||||
while let Some(row) = rows.try_next().await? {
|
||||
todos.push(row);
|
||||
}
|
||||
|
||||
// Add a random header(because why not)
|
||||
// let mut res_headers = HeaderMap::new();
|
||||
// res_headers.insert(SET_COOKIE, HeaderValue::from_str("fizz=buzz").unwrap());
|
||||
|
||||
// let res_parts = leptos_axum::ResponseParts {
|
||||
// headers: res_headers,
|
||||
// status: Some(StatusCode::IM_A_TEAPOT),
|
||||
// };
|
||||
|
||||
// let res_options_outer = use_context::<leptos_axum::ResponseOptions>();
|
||||
// if let Some(res_options) = res_options_outer {
|
||||
// res_options.overwrite(res_parts).await;
|
||||
// }
|
||||
|
||||
Ok(todos)
|
||||
}
|
||||
|
||||
#[server(AddTodo, "/api")]
|
||||
pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
let mut conn = db().await?;
|
||||
|
||||
// fake API delay
|
||||
std::thread::sleep(std::time::Duration::from_millis(1250));
|
||||
|
||||
match sqlx::query("INSERT INTO todos (title, completed) VALUES ($1, false)")
|
||||
.bind(title)
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
{
|
||||
Ok(_row) => Ok(()),
|
||||
Err(e) => Err(ServerFnError::ServerError(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
// The struct name and path prefix arguments are optional.
|
||||
#[server]
|
||||
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
|
||||
let mut conn = db().await?;
|
||||
|
||||
Ok(sqlx::query("DELETE FROM todos WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.map(|_| ())?)
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TodoApp() -> impl IntoView {
|
||||
//let id = use_context::<String>();
|
||||
provide_meta_context();
|
||||
view! {
|
||||
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<Stylesheet id="leptos" href="/pkg/todo_app_sqlite_csr.css"/>
|
||||
<Router>
|
||||
<header>
|
||||
<h1>"My Tasks"</h1>
|
||||
</header>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=Todos/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Todos() -> impl IntoView {
|
||||
let add_todo = create_server_multi_action::<AddTodo>();
|
||||
let delete_todo = create_server_action::<DeleteTodo>();
|
||||
let submissions = add_todo.submissions();
|
||||
|
||||
// list of todos is loaded from the server in reaction to changes
|
||||
let todos = create_resource(
|
||||
move || (add_todo.version().get(), delete_todo.version().get()),
|
||||
move |_| get_todos(),
|
||||
);
|
||||
|
||||
view! {
|
||||
<div>
|
||||
<MultiActionForm action=add_todo>
|
||||
<label>
|
||||
"Add a Todo"
|
||||
<input type="text" name="title"/>
|
||||
</label>
|
||||
<input type="submit" value="Add"/>
|
||||
</MultiActionForm>
|
||||
<Transition fallback=move || view! {<p>"Loading..."</p> }>
|
||||
<ErrorBoundary fallback=|errors| view!{<ErrorTemplate errors=errors/>}>
|
||||
{move || {
|
||||
let existing_todos = {
|
||||
move || {
|
||||
todos.get()
|
||||
.map(move |todos| match todos {
|
||||
Err(e) => {
|
||||
view! { <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view()
|
||||
}
|
||||
Ok(todos) => {
|
||||
if todos.is_empty() {
|
||||
view! { <p>"No tasks were found."</p> }.into_view()
|
||||
} else {
|
||||
todos
|
||||
.into_iter()
|
||||
.map(move |todo| {
|
||||
view! {
|
||||
|
||||
<li>
|
||||
{todo.title}
|
||||
<ActionForm action=delete_todo>
|
||||
<input type="hidden" name="id" value={todo.id}/>
|
||||
<input type="submit" value="X"/>
|
||||
</ActionForm>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
};
|
||||
|
||||
let pending_todos = move || {
|
||||
submissions
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter(|submission| submission.pending().get())
|
||||
.map(|submission| {
|
||||
view! {
|
||||
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
};
|
||||
|
||||
view! {
|
||||
|
||||
<ul>
|
||||
{existing_todos}
|
||||
{pending_todos}
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
}
|
||||
</ErrorBoundary>
|
||||
</Transition>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.pending {
|
||||
color: purple;
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
//! To run in this environment, you need to disable the default feature set and enable
|
||||
//! the `wasm` feature on `leptos_axum` in your `Cargo.toml`.
|
||||
//! ```toml
|
||||
//! leptos_axum = { version = "0.5.3", default-features = false, features = ["wasm"] }
|
||||
//! leptos_axum = { version = "0.5.2", default-features = false, features = ["wasm"] }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Features
|
||||
|
||||
@@ -192,11 +192,9 @@ mod error_boundary;
|
||||
pub use error_boundary::*;
|
||||
mod animated_show;
|
||||
mod for_loop;
|
||||
mod provider;
|
||||
mod show;
|
||||
pub use animated_show::*;
|
||||
pub use for_loop::*;
|
||||
pub use provider::*;
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
pub use serde;
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
use leptos::*;
|
||||
|
||||
#[component]
|
||||
/// Uses the context API to [`provide_context`] to its children and descendants,
|
||||
/// without overwriting any contexts of the same type in its own reactive scope.
|
||||
///
|
||||
/// This prevents issues related to “context shadowing.”
|
||||
///
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// #[component]
|
||||
/// pub fn App() -> impl IntoView {
|
||||
/// // each Provider will only provide the value to its children
|
||||
/// view! {
|
||||
/// <Provider value=1u8>
|
||||
/// // correctly gets 1 from context
|
||||
/// {use_context::<u8>().unwrap_or(0)}
|
||||
/// </Provider>
|
||||
/// <Provider value=2u8>
|
||||
/// // correctly gets 2 from context
|
||||
/// {use_context::<u8>().unwrap_or(0)}
|
||||
/// </Provider>
|
||||
/// // does not find any u8 in context
|
||||
/// {use_context::<u8>().unwrap_or(0)}
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn Provider<T>(
|
||||
/// The value to be provided via context.
|
||||
value: T,
|
||||
children: Children,
|
||||
) -> impl IntoView
|
||||
where
|
||||
T: Clone + 'static,
|
||||
{
|
||||
run_as_child(move || {
|
||||
provide_context(value);
|
||||
children()
|
||||
})
|
||||
}
|
||||
@@ -172,8 +172,7 @@ ssr = ["leptos_reactive/ssr"]
|
||||
nightly = ["leptos_reactive/nightly"]
|
||||
nonce = ["dep:base64", "dep:getrandom", "dep:rand"]
|
||||
experimental-islands = ["leptos_reactive/experimental-islands"]
|
||||
trace-component-props = []
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["nightly", "trace-component-props"]
|
||||
denylist = ["nightly"]
|
||||
skip_feature_sets = [["web", "ssr"]]
|
||||
|
||||
@@ -386,14 +386,11 @@ attr_signal_type_optional!(MaybeProp<T>);
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[doc(hidden)]
|
||||
#[inline(never)]
|
||||
#[track_caller]
|
||||
pub fn attribute_helper(
|
||||
el: &web_sys::Element,
|
||||
name: Oco<'static, str>,
|
||||
value: Attribute,
|
||||
) {
|
||||
#[cfg(debug_assertions)]
|
||||
let called_at = std::panic::Location::caller();
|
||||
use leptos_reactive::create_render_effect;
|
||||
match value {
|
||||
Attribute::Fn(f) => {
|
||||
@@ -401,26 +398,12 @@ pub fn attribute_helper(
|
||||
create_render_effect(move |old| {
|
||||
let new = f();
|
||||
if old.as_ref() != Some(&new) {
|
||||
attribute_expression(
|
||||
&el,
|
||||
&name,
|
||||
new.clone(),
|
||||
true,
|
||||
#[cfg(debug_assertions)]
|
||||
called_at,
|
||||
);
|
||||
attribute_expression(&el, &name, new.clone(), true);
|
||||
}
|
||||
new
|
||||
});
|
||||
}
|
||||
_ => attribute_expression(
|
||||
el,
|
||||
&name,
|
||||
value,
|
||||
false,
|
||||
#[cfg(debug_assertions)]
|
||||
called_at,
|
||||
),
|
||||
_ => attribute_expression(el, &name, value, false),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -431,7 +414,6 @@ pub(crate) fn attribute_expression(
|
||||
attr_name: &str,
|
||||
value: Attribute,
|
||||
force: bool,
|
||||
#[cfg(debug_assertions)] called_at: &'static std::panic::Location<'static>,
|
||||
) {
|
||||
use crate::HydrationCtx;
|
||||
|
||||
@@ -470,25 +452,10 @@ pub(crate) fn attribute_expression(
|
||||
}
|
||||
Attribute::Fn(f) => {
|
||||
let mut v = f();
|
||||
crate::debug_warn!(
|
||||
"At {called_at}, you are providing a dynamic attribute \
|
||||
with a nested function. For example, you might have a \
|
||||
closure that returns another function instead of a \
|
||||
value. This creates some added overhead. If possible, \
|
||||
you should instead provide a function that returns a \
|
||||
value instead.",
|
||||
);
|
||||
while let Attribute::Fn(f) = v {
|
||||
v = f();
|
||||
}
|
||||
attribute_expression(
|
||||
el,
|
||||
attr_name,
|
||||
v,
|
||||
force,
|
||||
#[cfg(debug_assertions)]
|
||||
called_at,
|
||||
);
|
||||
attribute_expression(el, attr_name, v, force);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ mod into_attribute;
|
||||
mod into_class;
|
||||
mod into_property;
|
||||
mod into_style;
|
||||
#[cfg(feature = "trace-component-props")]
|
||||
#[doc(hidden)]
|
||||
pub mod tracing_property;
|
||||
pub use into_attribute::*;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use wasm_bindgen::UnwrapThrowExt;
|
||||
|
||||
#[macro_export]
|
||||
/// Use for tracing property
|
||||
macro_rules! tracing_props {
|
||||
@@ -11,8 +9,9 @@ macro_rules! tracing_props {
|
||||
);
|
||||
};
|
||||
($($prop:tt),+ $(,)?) => {
|
||||
#[cfg(any(debug_assertions, feature = "ssr"))]
|
||||
{
|
||||
use ::leptos::leptos_dom::tracing_property::{Match, SerializeMatch, DefaultMatch};
|
||||
use ::leptos::leptos_dom::tracing_property::{Match, DebugMatch, DefaultMatch};
|
||||
let mut props = String::from('[');
|
||||
$(
|
||||
let prop = (&&Match {
|
||||
@@ -40,29 +39,17 @@ pub struct Match<T> {
|
||||
pub value: std::cell::Cell<Option<T>>,
|
||||
}
|
||||
|
||||
pub trait SerializeMatch {
|
||||
pub trait DebugMatch {
|
||||
type Return;
|
||||
fn spez(&self) -> Self::Return;
|
||||
}
|
||||
impl<T: serde::Serialize> SerializeMatch for &Match<&T> {
|
||||
impl<T: core::fmt::Debug> DebugMatch for &Match<&T> {
|
||||
type Return = String;
|
||||
fn spez(&self) -> Self::Return {
|
||||
let name = self.name;
|
||||
|
||||
// suppresses warnings when serializing signals into props
|
||||
#[cfg(debug_assertions)]
|
||||
let prev = leptos_reactive::SpecialNonReactiveZone::enter();
|
||||
|
||||
let value = serde_json::to_string(self.value.get().unwrap_throw())
|
||||
.map_or_else(
|
||||
|err| format!(r#"{{"name": "{name}", "error": "{err}"}}"#),
|
||||
|value| format!(r#"{{"name": "{name}", "value": {value}}}"#),
|
||||
);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
leptos_reactive::SpecialNonReactiveZone::exit(prev);
|
||||
|
||||
value
|
||||
let debug_value =
|
||||
format!("{:?}", self.value.get().unwrap()).replace('"', r#"\""#);
|
||||
format!(r#"{{"name": "{name}", "value": "{debug_value}"}}"#,)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +61,9 @@ impl<T> DefaultMatch for Match<&T> {
|
||||
type Return = String;
|
||||
fn spez(&self) -> Self::Return {
|
||||
let name = self.name;
|
||||
format!(r#"{{"name": "{name}", "value": "[unserializable value]"}}"#)
|
||||
format!(
|
||||
r#"{{"name": "{name}", "value": "[value does not implement Debug]"}}"#
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +76,7 @@ fn match_primitive() {
|
||||
value: std::cell::Cell::new(Some(&test)),
|
||||
})
|
||||
.spez();
|
||||
assert_eq!(prop, r#"{"name": "test", "value": "string"}"#);
|
||||
assert_eq!(prop, r#"{"name": "test", "value": "\"string\""}"#);
|
||||
|
||||
// &str
|
||||
let test = "string";
|
||||
@@ -96,7 +85,7 @@ fn match_primitive() {
|
||||
value: std::cell::Cell::new(Some(&test)),
|
||||
})
|
||||
.spez();
|
||||
assert_eq!(prop, r#"{"name": "test", "value": "string"}"#);
|
||||
assert_eq!(prop, r#"{"name": "test", "value": "\"string\""}"#);
|
||||
|
||||
// u128
|
||||
let test: u128 = 1;
|
||||
@@ -138,7 +127,7 @@ fn match_primitive() {
|
||||
#[test]
|
||||
fn match_serialize() {
|
||||
use serde::Serialize;
|
||||
#[derive(Serialize)]
|
||||
#[derive(Debug)]
|
||||
struct CustomStruct {
|
||||
field: &'static str,
|
||||
}
|
||||
@@ -149,7 +138,10 @@ fn match_serialize() {
|
||||
value: std::cell::Cell::new(Some(&test)),
|
||||
})
|
||||
.spez();
|
||||
assert_eq!(prop, r#"{"name": "test", "value": {"field":"field"}}"#);
|
||||
assert_eq!(
|
||||
prop,
|
||||
r#"{"name": "test", "value": "CustomStruct { field: \"field\" }"}"#
|
||||
);
|
||||
// Verification of ownership
|
||||
assert_eq!(test.field, "field");
|
||||
}
|
||||
@@ -170,7 +162,7 @@ fn match_no_serialize() {
|
||||
.spez();
|
||||
assert_eq!(
|
||||
prop,
|
||||
r#"{"name": "test", "value": "[unserializable value]"}"#
|
||||
r#"{"name": "test", "value": "[value does not implement Debug]"}"#
|
||||
);
|
||||
// Verification of ownership
|
||||
assert_eq!(test.field, "field");
|
||||
|
||||
@@ -43,8 +43,7 @@ ssr = ["server_fn_macro/ssr"]
|
||||
nightly = ["server_fn_macro/nightly"]
|
||||
tracing = []
|
||||
experimental-islands = []
|
||||
trace-component-props = []
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["nightly", "tracing", "trace-component-props"]
|
||||
denylist = ["nightly", "tracing"]
|
||||
skip_feature_sets = [["csr", "hydrate"], ["hydrate", "csr"], ["hydrate", "ssr"]]
|
||||
|
||||
@@ -118,6 +118,8 @@ impl ToTokens for Model {
|
||||
|
||||
let no_props = props.is_empty();
|
||||
|
||||
let mut body = body.to_owned();
|
||||
|
||||
// check for components that end ;
|
||||
if !is_transparent {
|
||||
let ends_semi =
|
||||
@@ -137,6 +139,7 @@ impl ToTokens for Model {
|
||||
}
|
||||
}
|
||||
|
||||
body.sig.ident = format_ident!("__{}", body.sig.ident);
|
||||
#[allow(clippy::redundant_clone)] // false positive
|
||||
let body_name = body.sig.ident.clone();
|
||||
|
||||
@@ -200,7 +203,7 @@ impl ToTokens for Model {
|
||||
#[cfg(debug_assertions)]
|
||||
let _guard = span.entered();
|
||||
},
|
||||
if no_props || !cfg!(feature = "trace-component-props") {
|
||||
if no_props {
|
||||
quote! {}
|
||||
} else {
|
||||
quote! {
|
||||
@@ -231,7 +234,6 @@ impl ToTokens for Model {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
let body_name = unmodified_fn_name_from_fn_name(&body_name);
|
||||
let body_expr = if *is_island {
|
||||
quote! {
|
||||
::leptos::SharedContext::with_hydration(move || {
|
||||
@@ -365,6 +367,7 @@ impl ToTokens for Model {
|
||||
.collect::<TokenStream>();
|
||||
|
||||
let body = quote! {
|
||||
#body
|
||||
#destructure_props
|
||||
#tracing_span_expr
|
||||
#component
|
||||
@@ -544,10 +547,10 @@ impl Model {
|
||||
/// used to improve IDEs and rust-analyzer's auto-completion behavior in case
|
||||
/// of a syntax error.
|
||||
pub struct DummyModel {
|
||||
pub attrs: Vec<Attribute>,
|
||||
pub vis: Visibility,
|
||||
pub sig: Signature,
|
||||
pub body: TokenStream,
|
||||
attrs: Vec<Attribute>,
|
||||
vis: Visibility,
|
||||
sig: Signature,
|
||||
body: TokenStream,
|
||||
}
|
||||
|
||||
impl Parse for DummyModel {
|
||||
@@ -585,21 +588,7 @@ impl ToTokens for DummyModel {
|
||||
let mut sig = sig.clone();
|
||||
sig.inputs.iter_mut().for_each(|arg| {
|
||||
if let FnArg::Typed(ty) = arg {
|
||||
ty.attrs.retain(|attr| match &attr.meta {
|
||||
Meta::List(list) => list
|
||||
.path
|
||||
.segments
|
||||
.first()
|
||||
.map(|n| n.ident != "prop")
|
||||
.unwrap_or(true),
|
||||
Meta::NameValue(name_value) => name_value
|
||||
.path
|
||||
.segments
|
||||
.first()
|
||||
.map(|n| n.ident != "doc")
|
||||
.unwrap_or(true),
|
||||
_ => true,
|
||||
});
|
||||
ty.attrs.clear();
|
||||
}
|
||||
});
|
||||
sig
|
||||
@@ -1173,7 +1162,3 @@ fn is_valid_into_view_return_type(ty: &ReturnType) -> bool {
|
||||
.iter()
|
||||
.any(|test| ty == test)
|
||||
}
|
||||
|
||||
pub fn unmodified_fn_name_from_fn_name(ident: &Ident) -> Ident {
|
||||
Ident::new(&format!("__{ident}"), ident.span())
|
||||
}
|
||||
|
||||
@@ -4,12 +4,11 @@
|
||||
#[macro_use]
|
||||
extern crate proc_macro_error;
|
||||
|
||||
use component::DummyModel;
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::{Span, TokenTree};
|
||||
use quote::ToTokens;
|
||||
use rstml::{node::KeyedAttribute, parse};
|
||||
use syn::{parse_macro_input, spanned::Spanned, token::Pub, Visibility};
|
||||
use syn::parse_macro_input;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Mode {
|
||||
@@ -32,7 +31,6 @@ impl Default for Mode {
|
||||
|
||||
mod params;
|
||||
mod view;
|
||||
use crate::component::unmodified_fn_name_from_fn_name;
|
||||
use view::{client_template::render_template, render_view};
|
||||
mod component;
|
||||
mod server;
|
||||
@@ -600,30 +598,21 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
false
|
||||
};
|
||||
|
||||
let mut dummy = syn::parse::<DummyModel>(s.clone());
|
||||
let parse_result = syn::parse::<component::Model>(s);
|
||||
let parse_result = syn::parse::<component::Model>(s.clone());
|
||||
|
||||
if let (Ok(ref mut unexpanded), Ok(model)) = (&mut dummy, parse_result) {
|
||||
let expanded = model.is_transparent(is_transparent).into_token_stream();
|
||||
unexpanded.sig.ident =
|
||||
unmodified_fn_name_from_fn_name(&unexpanded.sig.ident);
|
||||
quote! {
|
||||
#expanded
|
||||
#[doc(hidden)]
|
||||
#[allow(non_snake_case, dead_code, clippy::too_many_arguments)]
|
||||
#unexpanded
|
||||
}
|
||||
} else if let Ok(mut dummy) = dummy {
|
||||
dummy.sig.ident = unmodified_fn_name_from_fn_name(&dummy.sig.ident);
|
||||
quote! {
|
||||
#[doc(hidden)]
|
||||
#[allow(non_snake_case, dead_code, clippy::too_many_arguments)]
|
||||
#dummy
|
||||
}
|
||||
if let Ok(model) = parse_result {
|
||||
model
|
||||
.is_transparent(is_transparent)
|
||||
.into_token_stream()
|
||||
.into()
|
||||
} else {
|
||||
quote! {}
|
||||
// When the input syntax is invalid, e.g. while typing, we let
|
||||
// the dummy model output tokens similar to the input, which improves
|
||||
// IDEs and rust-analyzer's auto-complete capabilities.
|
||||
parse_macro_input!(s as component::DummyModel)
|
||||
.into_token_stream()
|
||||
.into()
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Defines a component as an interactive island when you are using the
|
||||
@@ -699,36 +688,28 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// ```
|
||||
#[proc_macro_error::proc_macro_error]
|
||||
#[proc_macro_attribute]
|
||||
pub fn island(_args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
let mut dummy = syn::parse::<DummyModel>(s.clone());
|
||||
let parse_result = syn::parse::<component::Model>(s);
|
||||
pub fn island(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
let is_transparent = if !args.is_empty() {
|
||||
let transparent = parse_macro_input!(args as syn::Ident);
|
||||
|
||||
if let (Ok(ref mut unexpanded), Ok(model)) = (&mut dummy, parse_result) {
|
||||
let expanded = model.is_island().into_token_stream();
|
||||
if !matches!(unexpanded.vis, Visibility::Public(_)) {
|
||||
unexpanded.vis = Visibility::Public(Pub {
|
||||
span: unexpanded.vis.span(),
|
||||
})
|
||||
}
|
||||
unexpanded.sig.ident =
|
||||
unmodified_fn_name_from_fn_name(&unexpanded.sig.ident);
|
||||
quote! {
|
||||
#expanded
|
||||
#[doc(hidden)]
|
||||
#[allow(non_snake_case, dead_code, clippy::too_many_arguments)]
|
||||
#unexpanded
|
||||
}
|
||||
} else if let Ok(mut dummy) = dummy {
|
||||
dummy.sig.ident = unmodified_fn_name_from_fn_name(&dummy.sig.ident);
|
||||
quote! {
|
||||
#[doc(hidden)]
|
||||
#[allow(non_snake_case, dead_code, clippy::too_many_arguments)]
|
||||
#dummy
|
||||
if transparent != "transparent" {
|
||||
abort!(
|
||||
transparent,
|
||||
"only `transparent` is supported";
|
||||
help = "try `#[island(transparent)]` or `#[island]`"
|
||||
);
|
||||
}
|
||||
|
||||
true
|
||||
} else {
|
||||
quote! {}
|
||||
}
|
||||
.into()
|
||||
false
|
||||
};
|
||||
|
||||
parse_macro_input!(s as component::Model)
|
||||
.is_transparent(is_transparent)
|
||||
.is_island()
|
||||
.into_token_stream()
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Annotates a struct so that it can be used with your Component as a `slot`.
|
||||
|
||||
@@ -13,6 +13,7 @@ fn unknown_prop_option(#[prop(hello)] test: bool) -> impl IntoView {
|
||||
|
||||
#[component]
|
||||
fn optional_and_optional_no_strip(
|
||||
,
|
||||
#[prop(optional, optional_no_strip)] conflicting: bool,
|
||||
) -> impl IntoView {
|
||||
_ = conflicting;
|
||||
@@ -20,6 +21,7 @@ fn optional_and_optional_no_strip(
|
||||
|
||||
#[component]
|
||||
fn optional_and_strip_option(
|
||||
,
|
||||
#[prop(optional, strip_option)] conflicting: bool,
|
||||
) -> impl IntoView {
|
||||
_ = conflicting;
|
||||
@@ -27,18 +29,23 @@ fn optional_and_strip_option(
|
||||
|
||||
#[component]
|
||||
fn optional_no_strip_and_strip_option(
|
||||
,
|
||||
#[prop(optional_no_strip, strip_option)] conflicting: bool,
|
||||
) -> impl IntoView {
|
||||
_ = conflicting;
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn default_without_value(#[prop(default)] default: bool) -> impl IntoView {
|
||||
fn default_without_value(
|
||||
,
|
||||
#[prop(default)] default: bool,
|
||||
) -> impl IntoView {
|
||||
_ = default;
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn default_with_invalid_value(
|
||||
,
|
||||
#[prop(default= |)] default: bool,
|
||||
) -> impl IntoView {
|
||||
_ = default;
|
||||
|
||||
@@ -1,3 +1,33 @@
|
||||
error: expected parameter name, found `,`
|
||||
--> tests/ui/component.rs:16:5
|
||||
|
|
||||
16 | ,
|
||||
| ^ expected parameter name
|
||||
|
||||
error: expected parameter name, found `,`
|
||||
--> tests/ui/component.rs:24:5
|
||||
|
|
||||
24 | ,
|
||||
| ^ expected parameter name
|
||||
|
||||
error: expected parameter name, found `,`
|
||||
--> tests/ui/component.rs:32:5
|
||||
|
|
||||
32 | ,
|
||||
| ^ expected parameter name
|
||||
|
||||
error: expected parameter name, found `,`
|
||||
--> tests/ui/component.rs:40:5
|
||||
|
|
||||
40 | ,
|
||||
| ^ expected parameter name
|
||||
|
||||
error: expected parameter name, found `,`
|
||||
--> tests/ui/component.rs:48:5
|
||||
|
|
||||
48 | ,
|
||||
| ^ expected parameter name
|
||||
|
||||
error: return type is incorrect
|
||||
--> tests/ui/component.rs:4:1
|
||||
|
|
||||
@@ -20,34 +50,32 @@ error: supported fields are `optional`, `optional_no_strip`, `strip_option`, `de
|
||||
10 | fn unknown_prop_option(#[prop(hello)] test: bool) -> impl IntoView {
|
||||
| ^^^^^
|
||||
|
||||
error: `optional` conflicts with mutually exclusive `optional_no_strip`
|
||||
--> tests/ui/component.rs:16:12
|
||||
error: expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
|
||||
--> tests/ui/component.rs:16:5
|
||||
|
|
||||
16 | #[prop(optional, optional_no_strip)] conflicting: bool,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
16 | ,
|
||||
| ^
|
||||
|
||||
error: `optional` conflicts with mutually exclusive `strip_option`
|
||||
--> tests/ui/component.rs:23:12
|
||||
error: expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
|
||||
--> tests/ui/component.rs:24:5
|
||||
|
|
||||
23 | #[prop(optional, strip_option)] conflicting: bool,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
24 | ,
|
||||
| ^
|
||||
|
||||
error: `optional_no_strip` conflicts with mutually exclusive `strip_option`
|
||||
--> tests/ui/component.rs:30:12
|
||||
error: expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
|
||||
--> tests/ui/component.rs:32:5
|
||||
|
|
||||
30 | #[prop(optional_no_strip, strip_option)] conflicting: bool,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
32 | ,
|
||||
| ^
|
||||
|
||||
error: unexpected end of input, expected assignment `=`
|
||||
--> tests/ui/component.rs:36:40
|
||||
error: expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
|
||||
--> tests/ui/component.rs:40:5
|
||||
|
|
||||
36 | fn default_without_value(#[prop(default)] default: bool) -> impl IntoView {
|
||||
| ^
|
||||
40 | ,
|
||||
| ^
|
||||
|
||||
error: unexpected end of input, expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
|
||||
|
||||
= help: try `#[prop(default=5 * 10)]`
|
||||
--> tests/ui/component.rs:42:22
|
||||
error: expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
|
||||
--> tests/ui/component.rs:48:5
|
||||
|
|
||||
42 | #[prop(default= |)] default: bool,
|
||||
| ^
|
||||
48 | ,
|
||||
| ^
|
||||
|
||||
@@ -84,29 +84,7 @@ use std::any::{Any, TypeId};
|
||||
/// that was provided in `<Parent/>`, meaning that the second `<Child/>` receives the context
|
||||
/// from its sibling instead.
|
||||
///
|
||||
/// ### Solution
|
||||
///
|
||||
/// If you are using the full Leptos framework, you can use the [`Provider`](leptos::Provider)
|
||||
/// component to solve this issue.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// #[component]
|
||||
/// fn Child() -> impl IntoView {
|
||||
/// let context = expect_context::<&'static str>();
|
||||
/// // creates a new reactive node, which means the context will
|
||||
/// // only be provided to its children, not modified in the parent
|
||||
/// view! {
|
||||
/// <Provider value="child_context">
|
||||
/// <div>{format!("child (context: {context})")}</div>
|
||||
/// </Provider>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ### Alternate Solution
|
||||
///
|
||||
/// This can also be solved by introducing some additional reactivity. In this case, it’s simplest
|
||||
/// This can be solved by introducing some additional reactivity. In this case, it’s simplest
|
||||
/// to simply make the body of `<Child/>` a function, which means it will be wrapped in a
|
||||
/// new reactive node when rendered:
|
||||
/// ```rust
|
||||
|
||||
@@ -1247,7 +1247,9 @@ where
|
||||
}
|
||||
#[cfg(all(feature = "hydrate", debug_assertions))]
|
||||
{
|
||||
if self.serializable != ResourceSerialization::Local {
|
||||
if self.serializable != ResourceSerialization::Local
|
||||
&& !SpecialNonReactiveZone::is_inside()
|
||||
{
|
||||
crate::macros::debug_warn!(
|
||||
"At {location}, you are reading a resource in \
|
||||
`hydrate` mode outside a <Suspense/> or \
|
||||
|
||||
@@ -94,7 +94,6 @@ impl Trigger {
|
||||
let diagnostics = diagnostics!(self);
|
||||
|
||||
with_runtime(|runtime| {
|
||||
runtime.update_if_necessary(self.id);
|
||||
self.id.subscribe(runtime, diagnostics);
|
||||
})
|
||||
.is_ok()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.5.3"
|
||||
version = "0.5.2"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.5.3"
|
||||
version = "0.5.2"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -52,7 +52,6 @@ pub fn Outlet() -> impl IntoView {
|
||||
prev_disposer.flatten()
|
||||
}
|
||||
(Some(child), _) => {
|
||||
drop(prev_disposer);
|
||||
is_showing.set(Some(child.id()));
|
||||
let (outlet, disposer) = build_outlet(child);
|
||||
set_outlet.set(Some(outlet));
|
||||
|
||||
@@ -484,11 +484,11 @@ fn root_route(
|
||||
|
||||
if prev.is_none() || !root_equal.get() {
|
||||
root.as_ref().map(|route| {
|
||||
drop(std::mem::take(
|
||||
&mut *root_disposer.borrow_mut(),
|
||||
));
|
||||
let (outlet, disposer) = outlet((*route).clone());
|
||||
*root_disposer.borrow_mut() = Some(disposer);
|
||||
drop(std::mem::replace(
|
||||
&mut *root_disposer.borrow_mut(),
|
||||
Some(disposer),
|
||||
));
|
||||
outlet
|
||||
})
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user