mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 14:52:35 -05:00
Compare commits
17 Commits
v0.3.1
...
gbj-patch-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cf6cbe84a | ||
|
|
87f6802967 | ||
|
|
2cbf3581c5 | ||
|
|
5a67e208fd | ||
|
|
3391a4a035 | ||
|
|
076aa363a4 | ||
|
|
2cb68c0bd4 | ||
|
|
6eb24b5017 | ||
|
|
b2faa6b86c | ||
|
|
43990b5b67 | ||
|
|
9453164dd2 | ||
|
|
00fcd1c65e | ||
|
|
85ad7b0f38 | ||
|
|
f0a9940364 | ||
|
|
b472aaf6a0 | ||
|
|
059c1bf61c | ||
|
|
add13fd6a4 |
28
Cargo.toml
28
Cargo.toml
@@ -25,22 +25,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.3.1"
|
||||
version = "0.3.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.3.1" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.3.1" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.3.1" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.3.1" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.3.1" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.3.1" }
|
||||
server_fn = { path = "./server_fn", default-features = false, version = "0.3.1" }
|
||||
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.3.1" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.3.1" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.3.1" }
|
||||
leptos_router = { path = "./router", version = "0.3.1" }
|
||||
leptos_meta = { path = "./meta", default-features = false, version = "0.3.1" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.3.1" }
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.3.0" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.3.0" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.3.0" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.3.0" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.3.0" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.3.0" }
|
||||
server_fn = { path = "./server_fn", default-features = false, version = "0.3.0" }
|
||||
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.3.0" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.3.0" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.3.0" }
|
||||
leptos_router = { path = "./router", version = "0.3.0" }
|
||||
leptos_meta = { path = "./meta", default-features = false, version = "0.3.0" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.3.0" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -41,6 +41,14 @@ build-std = ["std", "panic_abort", "core", "alloc"]
|
||||
build-std-features = ["panic_immediate_abort"]
|
||||
```
|
||||
|
||||
Note that if you're using this with SSR too, the same Cargo profile will be applied. You'll need to explicitly specify your target:
|
||||
```toml
|
||||
[build]
|
||||
target = "x86_64-unknown-linux-gnu" # or whatever
|
||||
```
|
||||
|
||||
And you'll need to add `panic = "abort"` to `[profile.release]` in `Cargo.toml`. Note that this applies the same `build-std` and panic settings to your server binary, which may not be desirable. Some further exploration is probably needed here.
|
||||
|
||||
5. One of the sources of binary size in WASM binaries can be `serde` serialization/deserialization code. Leptos uses `serde` by default to serialize and deserialize resources created with `create_resource`. You might try experimenting with the `miniserde` and `serde-lite` features, which allow you to use those crates for serialization and deserialization instead; each only implements a subset of `serde`’s functionality, but typically optimizes for size over speed.
|
||||
|
||||
## Things to Avoid
|
||||
|
||||
@@ -15,7 +15,7 @@ You _could_ do this by just creating two `<progress>` elements:
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let double_count = move || count() * 2;
|
||||
|
||||
view! {
|
||||
view! { cx,
|
||||
<progress
|
||||
max="50"
|
||||
value=count
|
||||
|
||||
5
examples/cargo-make/cargo-leptos-web-test.toml
Normal file
5
examples/cargo-make/cargo-leptos-web-test.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
[tasks.web-test]
|
||||
dependencies = ["cargo-leptos-e2e"]
|
||||
|
||||
[tasks.clean-all]
|
||||
dependencies = ["clean-cargo", "clean-node_modules", "clean-playwright"]
|
||||
@@ -1,3 +1,6 @@
|
||||
[env]
|
||||
END2END_DIR = "end2end"
|
||||
|
||||
[tasks.pre-clippy]
|
||||
env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features -- -D warnings" }
|
||||
|
||||
@@ -5,6 +8,9 @@ env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features -- -D warnings" }
|
||||
description = "Check for style violations"
|
||||
dependencies = ["check-format-flow", "clippy-flow"]
|
||||
|
||||
[tasks.check-format]
|
||||
args = ["fmt", "--", "--check", "--config-path", "../../"]
|
||||
|
||||
[tasks.verify-local]
|
||||
description = "Run all quality checks and tests from an example directory"
|
||||
dependencies = ["check-style", "test-local"]
|
||||
@@ -13,11 +19,62 @@ dependencies = ["check-style", "test-local"]
|
||||
description = "Run all tests from an example directory"
|
||||
dependencies = ["test", "web-test"]
|
||||
|
||||
[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"
|
||||
cwd = "${END2END_DIR}"
|
||||
command = "rm"
|
||||
args = ["-rf", "playwright", "playwright/.cache", "test-results"]
|
||||
|
||||
[tasks.clean-all]
|
||||
dependencies = ["clean", "clean-trunk"]
|
||||
description = "Delete all temporary directories"
|
||||
category = "Cleanup"
|
||||
dependencies = ["clean-cargo"]
|
||||
|
||||
[tasks.wasm-web-test]
|
||||
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]
|
||||
description = "Setup e2e dependencies"
|
||||
cwd = "${END2END_DIR}"
|
||||
script = '''
|
||||
BOLD="\e[1m"
|
||||
GREEN="\e[0;32m"
|
||||
RED="\e[0;31m"
|
||||
RESET="\e[0m"
|
||||
|
||||
if command -v pnpm; then
|
||||
pnpm install
|
||||
elif command -v npm; then
|
||||
npm install
|
||||
else
|
||||
echo "${RED}${BOLD}ERROR${RESET} - pnpm or npm is required by this task"
|
||||
exit 1
|
||||
fi
|
||||
'''
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
[tasks.web-test]
|
||||
env = { CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome" }
|
||||
command = "cargo"
|
||||
args = ["make", "wasm-pack-test"]
|
||||
dependencies = ["wasm-web-test"]
|
||||
|
||||
[tasks.clean-all]
|
||||
dependencies = ["clean-cargo", "clean-trunk"]
|
||||
|
||||
@@ -10,12 +10,12 @@ crate-type = ["cdylib", "rlib"]
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_axum = { path = "../../../leptos/integrations/axum", default-features = false, optional = true }
|
||||
leptos_meta = { path = "../../../leptos/meta", default-features = false }
|
||||
leptos_router = { path = "../../../leptos/router", default-features = false }
|
||||
leptos_axum = { path = "../../integrations/axum", default-features = false, optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4.17"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
simple_logger = "4.0.0"
|
||||
|
||||
@@ -3,7 +3,7 @@ use cfg_if::cfg_if;
|
||||
cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::Extension,
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
@@ -14,7 +14,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use leptos::{LeptosOptions, view};
|
||||
use crate::landing::App;
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
let options = &*options;
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
@@ -5,7 +5,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use crate::landing::*;
|
||||
use axum::body::Body as AxumBody;
|
||||
use axum::{
|
||||
extract::{Extension, Path},
|
||||
extract::{State, Path},
|
||||
http::Request,
|
||||
response::{IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
@@ -21,7 +21,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
#[cfg(feature = "ssr")]
|
||||
async fn custom_handler(
|
||||
Path(id): Path<String>,
|
||||
Extension(options): Extension<Arc<LeptosOptions>>,
|
||||
State(options): State<Arc<LeptosOptions>>,
|
||||
req: Request<AxumBody>,
|
||||
) -> Response {
|
||||
let handler = leptos_axum::render_app_to_stream_with_context(
|
||||
@@ -44,7 +44,7 @@ async fn main() {
|
||||
|
||||
// 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 leptos_options = Arc::new(conf.leptos_options);
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
|
||||
|
||||
@@ -58,7 +58,7 @@ async fn main() {
|
||||
|cx| view! { cx, <App/> },
|
||||
)
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(Extension(Arc::new(leptos_options)));
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
|
||||
@@ -4,7 +4,7 @@ cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::Extension,
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
@@ -15,7 +15,7 @@ if #[cfg(feature = "ssr")] {
|
||||
use leptos::{LeptosOptions};
|
||||
use crate::error_template::error_template;
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
let options = &*options;
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
@@ -7,7 +7,6 @@ if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
Router,
|
||||
routing::get,
|
||||
extract::Extension,
|
||||
};
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use std::sync::Arc;
|
||||
@@ -18,7 +17,7 @@ if #[cfg(feature = "ssr")] {
|
||||
use hackernews_axum::*;
|
||||
|
||||
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let leptos_options = Arc::new(conf.leptos_options);
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
|
||||
|
||||
@@ -29,7 +28,7 @@ if #[cfg(feature = "ssr")] {
|
||||
.route("/favicon.ico", get(file_and_error_handler))
|
||||
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> } )
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(Extension(Arc::new(leptos_options)));
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
|
||||
@@ -4,7 +4,7 @@ cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::Extension,
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
@@ -16,7 +16,7 @@ if #[cfg(feature = "ssr")] {
|
||||
use crate::error_template::ErrorTemplate;
|
||||
use crate::errors::TodoAppError;
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
let options = &*options;
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
@@ -6,7 +6,7 @@ if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
response::{Response, IntoResponse},
|
||||
routing::get,
|
||||
extract::{Path, Extension, RawQuery},
|
||||
extract::{Path, State, Extension, RawQuery},
|
||||
http::{Request, header::HeaderMap},
|
||||
body::Body as AxumBody,
|
||||
Router,
|
||||
@@ -33,7 +33,7 @@ if #[cfg(feature = "ssr")] {
|
||||
}, request).await
|
||||
}
|
||||
|
||||
async fn leptos_routes_handler(Extension(pool): Extension<SqlitePool>, auth_session: AuthSession, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
|
||||
async fn leptos_routes_handler(Extension(pool): Extension<SqlitePool>, auth_session: AuthSession, State(options): State<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
|
||||
let handler = leptos_axum::render_app_to_stream_with_context((*options).clone(),
|
||||
move |cx| {
|
||||
provide_context(cx, auth_session.clone());
|
||||
@@ -68,7 +68,7 @@ if #[cfg(feature = "ssr")] {
|
||||
|
||||
// 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 leptos_options = Arc::new(conf.leptos_options);
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(|cx| view! { cx, <TodoApp/> }).await;
|
||||
|
||||
@@ -80,8 +80,8 @@ if #[cfg(feature = "ssr")] {
|
||||
.layer(AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(Some(pool.clone()))
|
||||
.with_config(auth_config))
|
||||
.layer(SessionLayer::new(session_store))
|
||||
.layer(Extension(Arc::new(leptos_options)))
|
||||
.layer(Extension(pool));
|
||||
.layer(Extension(pool))
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
|
||||
@@ -3,7 +3,7 @@ use cfg_if::cfg_if;
|
||||
cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::Extension,
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
@@ -14,7 +14,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use leptos::{LeptosOptions, view};
|
||||
use crate::app::App;
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
let options = &*options;
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::{extract::Extension, routing::post, Router};
|
||||
use axum::{routing::post, Router};
|
||||
use leptos::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use ssr_modes_axum::{app::*, fallback::file_and_error_handler};
|
||||
@@ -9,7 +9,7 @@ async fn main() {
|
||||
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let leptos_options = conf.leptos_options;
|
||||
let leptos_options = Arc::new(conf.leptos_options);
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
|
||||
|
||||
@@ -24,7 +24,7 @@ async fn main() {
|
||||
|cx| view! { cx, <App/> },
|
||||
)
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(Extension(Arc::new(leptos_options)));
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
|
||||
7
examples/tailwind/.gitignore
vendored
7
examples/tailwind/.gitignore
vendored
@@ -8,3 +8,10 @@ Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# Support playwright testing
|
||||
node_modules/
|
||||
test-results/
|
||||
end2end/playwright-report/
|
||||
playwright/.cache/
|
||||
pnpm-lock.yaml
|
||||
|
||||
@@ -96,6 +96,7 @@ site-addr = "127.0.0.1:3000"
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
end2end-cmd = "npx playwright test"
|
||||
end2end-dir = "end2end"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
extend = [{ path = "../cargo-make/common.toml" }]
|
||||
extend = [
|
||||
{ path = "../cargo-make/common.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos-web-test.toml" },
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
|
||||
@@ -104,3 +104,8 @@ You'll need to install trunk to client side render this bundle.
|
||||
## Attribution
|
||||
|
||||
Many thanks to GreatGreg for putting together this guide. You can find the original, with added details, [here](https://github.com/leptos-rs/leptos/discussions/125).
|
||||
|
||||
## Playwright Testing
|
||||
|
||||
- Run `cargo make setup` to install dependencies
|
||||
- Run `cargo leptos test` or `cargo leptos end-to-end` to run the tests
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("homepage has title and links to intro page", async ({ page }) => {
|
||||
test("should see the welcome message", async ({ page }) => {
|
||||
await page.goto("http://localhost:3000/");
|
||||
|
||||
await expect(page).toHaveTitle("Cargo Leptos");
|
||||
|
||||
await expect(page.locator("h1")).toHaveText("Hi from your Leptos WASM!");
|
||||
await expect(page.locator("h2")).toHaveText("Welcome to Leptos with Tailwind");
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::Extension,
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
@@ -16,7 +16,7 @@ if #[cfg(feature = "ssr")] {
|
||||
use crate::error_template::ErrorTemplate;
|
||||
use crate::errors::TodoAppError;
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
let options = &*options;
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
@@ -5,7 +5,7 @@ cfg_if! {
|
||||
use leptos::*;
|
||||
use axum::{
|
||||
routing::{post, get},
|
||||
extract::{Extension, Path},
|
||||
extract::{State, Path},
|
||||
http::Request,
|
||||
response::{IntoResponse, Response},
|
||||
Router,
|
||||
@@ -18,7 +18,7 @@ cfg_if! {
|
||||
use std::sync::Arc;
|
||||
|
||||
//Define a handler to test extractor with state
|
||||
async fn custom_handler(Path(id): Path<String>, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
|
||||
async fn custom_handler(Path(id): Path<String>, State(options): State<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
|
||||
let handler = leptos_axum::render_app_to_stream_with_context((*options).clone(),
|
||||
move |cx| {
|
||||
provide_context(cx, id.clone());
|
||||
@@ -42,7 +42,7 @@ cfg_if! {
|
||||
|
||||
// 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 leptos_options = Arc::new(conf.leptos_options);
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(|cx| view! { cx, <TodoApp/> }).await;
|
||||
|
||||
@@ -52,7 +52,7 @@ cfg_if! {
|
||||
.route("/special/:id", get(custom_handler))
|
||||
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <TodoApp/> } )
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(Extension(Arc::new(leptos_options)));
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
|
||||
@@ -146,11 +146,8 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
</header>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=|cx| view! {
|
||||
cx,
|
||||
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
|
||||
<Todos/>
|
||||
</ErrorBoundary>
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<Todos/>
|
||||
}/> //Route
|
||||
<Route path="weird" methods=&[Method::Get, Method::Post]
|
||||
ssr=SsrMode::Async
|
||||
@@ -203,63 +200,65 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
<input type="submit" value="Add"/>
|
||||
</MultiActionForm>
|
||||
<Transition fallback=move || view! {cx, <p>"Loading..."</p> }>
|
||||
{move || {
|
||||
let existing_todos = {
|
||||
move || {
|
||||
todos.read(cx)
|
||||
.map(move |todos| match todos {
|
||||
Err(e) => {
|
||||
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
|
||||
}
|
||||
Ok(todos) => {
|
||||
if todos.is_empty() {
|
||||
view! { cx, <p>"No tasks were found."</p> }.into_view(cx)
|
||||
} else {
|
||||
todos
|
||||
.into_iter()
|
||||
.map(move |todo| {
|
||||
view! {
|
||||
cx,
|
||||
<li>
|
||||
{todo.title}
|
||||
<ActionForm action=delete_todo>
|
||||
<input type="hidden" name="id" value={todo.id}/>
|
||||
<input type="submit" value="X"/>
|
||||
</ActionForm>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect_view(cx)
|
||||
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
|
||||
{move || {
|
||||
let existing_todos = {
|
||||
move || {
|
||||
todos.read(cx)
|
||||
.map(move |todos| match todos {
|
||||
Err(e) => {
|
||||
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
};
|
||||
|
||||
let pending_todos = move || {
|
||||
submissions
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter(|submission| submission.pending().get())
|
||||
.map(|submission| {
|
||||
view! {
|
||||
cx,
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
Ok(todos) => {
|
||||
if todos.is_empty() {
|
||||
view! { cx, <p>"No tasks were found."</p> }.into_view(cx)
|
||||
} else {
|
||||
todos
|
||||
.into_iter()
|
||||
.map(move |todo| {
|
||||
view! {
|
||||
cx,
|
||||
<li>
|
||||
{todo.title}
|
||||
<ActionForm action=delete_todo>
|
||||
<input type="hidden" name="id" value={todo.id}/>
|
||||
<input type="submit" value="X"/>
|
||||
</ActionForm>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect_view(cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
})
|
||||
.collect_view(cx)
|
||||
};
|
||||
};
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<ul>
|
||||
{existing_todos}
|
||||
{pending_todos}
|
||||
</ul>
|
||||
let pending_todos = move || {
|
||||
submissions
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter(|submission| submission.pending().get())
|
||||
.map(|submission| {
|
||||
view! {
|
||||
cx,
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
}
|
||||
})
|
||||
.collect_view(cx)
|
||||
};
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<ul>
|
||||
{existing_todos}
|
||||
{pending_todos}
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</ErrorBoundary>
|
||||
</Transition>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ impl Todos {
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, id: Uuid) {
|
||||
self.0.retain(|todo| todo.id != id);
|
||||
self.retain(|todo| todo.id != id);
|
||||
}
|
||||
|
||||
pub fn remaining(&self) -> usize {
|
||||
@@ -76,7 +76,23 @@ impl Todos {
|
||||
}
|
||||
|
||||
fn clear_completed(&mut self) {
|
||||
self.0.retain(|todo| !todo.completed.get());
|
||||
self.retain(|todo| !todo.completed.get());
|
||||
}
|
||||
|
||||
fn retain(&mut self, mut f: impl FnMut(&Todo) -> bool) {
|
||||
self.0.retain(|todo| {
|
||||
let retain = f(todo);
|
||||
// because these signals are created at the top level,
|
||||
// they are owned by the <TodoMVC/> component and not
|
||||
// by the individual <Todo/> components. This means
|
||||
// that if they are not manually disposed when removed, they
|
||||
// will be held onto until the <TodoMVC/> is unmounted.
|
||||
if !retain {
|
||||
todo.title.dispose();
|
||||
todo.completed.dispose();
|
||||
}
|
||||
retain
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +152,7 @@ pub fn TodoMVC(cx: Scope) -> impl IntoView {
|
||||
|
||||
// Handle the three filter modes: All, Active, and Completed
|
||||
let (mode, set_mode) = create_signal(cx, Mode::All);
|
||||
window_event_listener_untyped("hashchange", move |_| {
|
||||
window_event_listener(ev::hashchange, move |_| {
|
||||
let new_mode =
|
||||
location_hash().map(|hash| route(&hash)).unwrap_or_default();
|
||||
set_mode(new_mode);
|
||||
|
||||
@@ -8,6 +8,7 @@ repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Actix integrations for the Leptos web framework."
|
||||
|
||||
[dependencies]
|
||||
actix-http = "3"
|
||||
actix-web = "4"
|
||||
futures = "0.3"
|
||||
leptos = { workspace = true, features = ["ssr"] }
|
||||
|
||||
@@ -185,7 +185,7 @@ pub fn handle_server_fns_with_context(
|
||||
.and_then(|value| value.to_str().ok());
|
||||
|
||||
if let Some(server_fn) = server_fn_by_path(path.as_str()) {
|
||||
let body: &[u8] = &body;
|
||||
let body_ref: &[u8] = &body;
|
||||
|
||||
let runtime = create_runtime();
|
||||
let (cx, disposer) = raw_scope_and_disposer(runtime);
|
||||
@@ -198,10 +198,28 @@ pub fn handle_server_fns_with_context(
|
||||
provide_context(cx, req.clone());
|
||||
provide_context(cx, res_options.clone());
|
||||
|
||||
// we consume the body here (using the web::Bytes extractor), but it is required for things
|
||||
// like MultipartForm
|
||||
if req
|
||||
.headers()
|
||||
.get("Content-Type")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| {
|
||||
Some(
|
||||
value.starts_with(
|
||||
"multipart/form-data; boundary=",
|
||||
),
|
||||
)
|
||||
})
|
||||
== Some(true)
|
||||
{
|
||||
provide_context(cx, body.clone());
|
||||
}
|
||||
|
||||
let query = req.query_string().as_bytes();
|
||||
|
||||
let data = match &server_fn.encoding {
|
||||
Encoding::Url | Encoding::Cbor => body,
|
||||
Encoding::Url | Encoding::Cbor => body_ref,
|
||||
Encoding::GetJSON | Encoding::GetCBOR => query,
|
||||
};
|
||||
let res = match (server_fn.trait_obj)(cx, data).await {
|
||||
@@ -1028,7 +1046,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper to make it easier to use Axum extractors in server functions. This takes
|
||||
/// A helper to make it easier to use Actix extractors in server functions. This takes
|
||||
/// a handler function as its argument. The handler follows similar rules to an Actix
|
||||
/// [Handler](actix_web::Handler): it is an async function that receives arguments that
|
||||
/// will be extracted from the request and returns some value.
|
||||
@@ -1072,9 +1090,17 @@ where
|
||||
{
|
||||
let req = use_context::<actix_web::HttpRequest>(cx)
|
||||
.expect("HttpRequest should have been provided via context");
|
||||
let input = E::extract(&req)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
|
||||
|
||||
let input = if let Some(body) = use_context::<Bytes>(cx) {
|
||||
let (_, mut payload) = actix_http::h1::Payload::create(false);
|
||||
payload.unread_data(body);
|
||||
E::from_request(&req, &mut dev::Payload::from(payload))
|
||||
} else {
|
||||
E::extract(&req)
|
||||
}
|
||||
.await
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
|
||||
|
||||
Ok(f.call(input).await)
|
||||
}
|
||||
|
||||
|
||||
@@ -1126,12 +1126,39 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// This trait allows one to use your custom struct in Axum's router, provided it can provide the
|
||||
/// `LeptosOptions` to use for the `LeptosRoutes` trait functions.
|
||||
pub trait LeptosOptionProvider {
|
||||
fn options(&self) -> LeptosOptions;
|
||||
}
|
||||
|
||||
/// Implement `LeptosOptionProvider` trait for `LeptosOptions` itself.
|
||||
impl LeptosOptionProvider for LeptosOptions {
|
||||
fn options(&self) -> LeptosOptions {
|
||||
self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement `LeptosOptionProvider` trait for any type wrapped in an Arc, if that type implements
|
||||
/// `LeptosOptionProvider` as states in axum are often provided wrapped in an Arc.
|
||||
impl<T> LeptosOptionProvider for Arc<T>
|
||||
where
|
||||
T: LeptosOptionProvider,
|
||||
{
|
||||
fn options(&self) -> LeptosOptions {
|
||||
(**self).options()
|
||||
}
|
||||
}
|
||||
|
||||
/// This trait allows one to pass a list of routes and a render function to Axum's router, letting us avoid
|
||||
/// having to use wildcards or manually define all routes in multiple places.
|
||||
pub trait LeptosRoutes {
|
||||
pub trait LeptosRoutes<OP>
|
||||
where
|
||||
OP: LeptosOptionProvider,
|
||||
{
|
||||
fn leptos_routes<IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
options: OP,
|
||||
paths: Vec<RouteListing>,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
) -> Self
|
||||
@@ -1140,7 +1167,7 @@ pub trait LeptosRoutes {
|
||||
|
||||
fn leptos_routes_with_context<IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
options: OP,
|
||||
paths: Vec<RouteListing>,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
@@ -1154,16 +1181,20 @@ pub trait LeptosRoutes {
|
||||
handler: H,
|
||||
) -> Self
|
||||
where
|
||||
H: axum::handler::Handler<T, (), axum::body::Body>,
|
||||
H: axum::handler::Handler<T, OP, axum::body::Body>,
|
||||
T: 'static;
|
||||
}
|
||||
|
||||
/// The default implementation of `LeptosRoutes` which takes in a list of paths, and dispatches GET requests
|
||||
/// to those paths to Leptos's renderer.
|
||||
impl LeptosRoutes for axum::Router {
|
||||
impl<OP> LeptosRoutes<OP> for axum::Router<OP>
|
||||
where
|
||||
OP: LeptosOptionProvider + Clone + Send + Sync + 'static,
|
||||
{
|
||||
#[tracing::instrument(level = "info", fields(error), skip_all)]
|
||||
fn leptos_routes<IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
options: OP,
|
||||
paths: Vec<RouteListing>,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
) -> Self
|
||||
@@ -1176,7 +1207,7 @@ impl LeptosRoutes for axum::Router {
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
fn leptos_routes_with_context<IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
options: OP,
|
||||
paths: Vec<RouteListing>,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
@@ -1194,7 +1225,7 @@ impl LeptosRoutes for axum::Router {
|
||||
match listing.mode() {
|
||||
SsrMode::OutOfOrder => {
|
||||
let s = render_app_to_stream_with_context(
|
||||
options.clone(),
|
||||
options.options(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
@@ -1208,7 +1239,7 @@ impl LeptosRoutes for axum::Router {
|
||||
}
|
||||
SsrMode::PartiallyBlocked => {
|
||||
let s = render_app_to_stream_with_context_and_replace_blocks(
|
||||
options.clone(),
|
||||
options.options(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
true
|
||||
@@ -1223,7 +1254,7 @@ impl LeptosRoutes for axum::Router {
|
||||
}
|
||||
SsrMode::InOrder => {
|
||||
let s = render_app_to_stream_in_order_with_context(
|
||||
options.clone(),
|
||||
options.options(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
@@ -1237,7 +1268,7 @@ impl LeptosRoutes for axum::Router {
|
||||
}
|
||||
SsrMode::Async => {
|
||||
let s = render_app_async_with_context(
|
||||
options.clone(),
|
||||
options.options(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
@@ -1263,7 +1294,7 @@ impl LeptosRoutes for axum::Router {
|
||||
handler: H,
|
||||
) -> Self
|
||||
where
|
||||
H: axum::handler::Handler<T, (), axum::body::Body>,
|
||||
H: axum::handler::Handler<T, OP, axum::body::Body>,
|
||||
T: 'static,
|
||||
{
|
||||
let mut router = self;
|
||||
@@ -1286,6 +1317,7 @@ impl LeptosRoutes for axum::Router {
|
||||
router
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
fn get_leptos_pool() -> LocalPoolHandle {
|
||||
static LOCAL_POOL: OnceCell<LocalPoolHandle> = OnceCell::new();
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
//!
|
||||
//! And you can do all three of these **using the same Leptos code.**
|
||||
//!
|
||||
//! Take a look at the [Leptos Book](https://leptos-rs.github.io/leptos/) for a walkthrough of the framework.
|
||||
//! Join us on our [Discord Channel](https://discord.gg/v38Eef6sWG) to see what the community is building.
|
||||
//! Explore our [Examples](https://github.com/leptos-rs/leptos/tree/main/examples) to see Leptos in action.
|
||||
//!
|
||||
//! # `nightly` Note
|
||||
//! Most of the examples assume you’re using `nightly` Rust. If you’re on stable, note the following:
|
||||
//! 1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.0", features = ["stable"] }`
|
||||
|
||||
@@ -101,13 +101,17 @@ where
|
||||
use leptos_reactive::signal_prelude::*;
|
||||
|
||||
// run the child; we'll probably throw this away, but it will register resource reads
|
||||
let child = orig_child(cx).into_view(cx);
|
||||
let _child = orig_child(cx).into_view(cx);
|
||||
let after_original_child = HydrationCtx::id();
|
||||
|
||||
let initial = {
|
||||
// no resources were read under this, so just return the child
|
||||
if context.pending_resources.get() == 0 {
|
||||
child
|
||||
let orig_child = Rc::clone(&orig_child);
|
||||
HydrationCtx::continue_from(current_id.clone());
|
||||
Fragment::lazy(Box::new(move || {
|
||||
vec![DynChild::new(move || orig_child(cx)).into_view(cx)]
|
||||
})).into_view(cx)
|
||||
}
|
||||
// show the fallback, but also prepare to stream HTML
|
||||
else {
|
||||
|
||||
@@ -17,7 +17,7 @@ leptos_actix = { path = "../../../../integrations/actix", optional = true }
|
||||
leptos_router = { path = "../../../../router", default-features = false }
|
||||
log = "0.4"
|
||||
simple_logger = "4"
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen = "0.2.85"
|
||||
serde = "1.0.159"
|
||||
tokio = { version = "1.27.0", features = ["time"], optional = true }
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
<Route path="single" view=|cx| view! { cx, <Single/> }/>
|
||||
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
|
||||
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
|
||||
<Route path="none" view=|cx| view! { cx, <None/> }/>
|
||||
</Route>
|
||||
// in-order
|
||||
<Route
|
||||
@@ -71,6 +72,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
<Route path="single" view=|cx| view! { cx, <Single/> }/>
|
||||
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
|
||||
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
|
||||
<Route path="none" view=|cx| view! { cx, <None/> }/>
|
||||
</Route>
|
||||
// async
|
||||
<Route
|
||||
@@ -86,6 +88,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
<Route path="single" view=|cx| view! { cx, <Single/> }/>
|
||||
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
|
||||
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
|
||||
<Route path="none" view=|cx| view! { cx, <None/> }/>
|
||||
</Route>
|
||||
</Routes>
|
||||
</main>
|
||||
@@ -101,6 +104,7 @@ fn SecondaryNav(cx: Scope) -> impl IntoView {
|
||||
<A href="single">"Single"</A>
|
||||
<A href="parallel">"Parallel"</A>
|
||||
<A href="inside-component">"Inside Component"</A>
|
||||
<A href="none">"No Resources"</A>
|
||||
</nav>
|
||||
}
|
||||
}
|
||||
@@ -217,3 +221,25 @@ fn InsideComponentChild(cx: Scope) -> impl IntoView {
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn None(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
|
||||
view! { cx,
|
||||
<div>
|
||||
<Suspense fallback=|| "Loading 1...">
|
||||
<div>"Children inside Suspense should hydrate properly."</div>
|
||||
<button on:click=move |_| set_count.update(|n| *n += 1)>
|
||||
{count}
|
||||
</button>
|
||||
</Suspense>
|
||||
<p>"Children following " <code>"<Suspense/>"</code> " should hydrate properly."</p>
|
||||
<div>
|
||||
<button on:click=move |_| set_count.update(|n| *n += 1)>
|
||||
{count}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ pub trait EventDescriptor: Clone {
|
||||
}
|
||||
}
|
||||
|
||||
/// Overrides the [`EventDescriptor::bubbles`] method to always return
|
||||
/// Overrides the [`EventDescriptor::BUBBLES`] value to always return
|
||||
/// `false`, which forces the event to not be globally delegated.
|
||||
#[derive(Clone)]
|
||||
#[allow(non_camel_case_types)]
|
||||
|
||||
@@ -807,7 +807,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
/// Sets a style on an element.
|
||||
///
|
||||
/// **Note**: In the builder syntax, this will be overwritten by the `style`
|
||||
/// attribute if you use `.attr("class", /* */)`. In the `view` macro, they
|
||||
/// attribute if you use `.attr("style", /* */)`. In the `view` macro, they
|
||||
/// are automatically re-ordered so that this over-writing does not happen.
|
||||
#[track_caller]
|
||||
pub fn style(
|
||||
|
||||
@@ -240,13 +240,20 @@ impl View {
|
||||
dont_escape_text: bool,
|
||||
) {
|
||||
match self {
|
||||
View::Suspense(id, _) => {
|
||||
View::Suspense(id, view) => {
|
||||
let id = id.to_string();
|
||||
if let Some(data) = cx.take_pending_fragment(&id) {
|
||||
chunks.push_back(StreamChunk::Async {
|
||||
chunks: data.in_order,
|
||||
should_block: data.should_block,
|
||||
});
|
||||
} else {
|
||||
// if not registered, means it was already resolved
|
||||
View::CoreComponent(view).into_stream_chunks_helper(
|
||||
cx,
|
||||
chunks,
|
||||
dont_escape_text,
|
||||
);
|
||||
}
|
||||
}
|
||||
View::Text(node) => {
|
||||
|
||||
@@ -672,7 +672,7 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// Annotates a struct so that it can be used with your Component as a `slot`.
|
||||
///
|
||||
/// The `#[slot]` macro allows you to annotate plain Rust struct as component slots and use them
|
||||
/// within your Leptos [component](crate::component!) properties. The struct can contain any number
|
||||
/// within your Leptos [`component`](macro@crate::component) properties. The struct can contain any number
|
||||
/// of fields. When you use the component somewhere else, the names of the slot fields are the
|
||||
/// names of the properties you use in the [view](crate::view!) macro.
|
||||
///
|
||||
|
||||
@@ -42,7 +42,7 @@ web-sys = { version = "0.3", optional = true, features = [
|
||||
] }
|
||||
cfg-if = "1"
|
||||
indexmap = "1"
|
||||
self_cell = "1.0.0"
|
||||
ouroboros = { version = "0.15.6", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.4.0", features = ["html_reports"] }
|
||||
|
||||
@@ -188,6 +188,7 @@ impl Runtime {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::await_holding_refcell_ref)] // not using this part of ouroboros
|
||||
pub(crate) fn mark_dirty(&self, node: NodeId) {
|
||||
//crate::macros::debug_warn!("marking {node:?} dirty");
|
||||
let mut nodes = self.nodes.borrow_mut();
|
||||
@@ -216,24 +217,23 @@ impl Runtime {
|
||||
* `Check` or `DirtyMarked`.
|
||||
*
|
||||
* Because `RefCell`, borrowing the iterators all at once is difficult,
|
||||
* so a self-referential struct is used instead. self_cell produces safe
|
||||
* so a self-referential struct is used instead. ouroboros produces safe
|
||||
* code, but it would not be recommended to use this outside of this
|
||||
* algorithm.
|
||||
*/
|
||||
|
||||
type Dependent<'a> = indexmap::set::Iter<'a, NodeId>;
|
||||
#[ouroboros::self_referencing]
|
||||
struct RefIter<'a> {
|
||||
set: std::cell::Ref<'a, FxIndexSet<NodeId>>,
|
||||
|
||||
self_cell::self_cell! {
|
||||
struct RefIter<'a> {
|
||||
owner: std::cell::Ref<'a, FxIndexSet<NodeId>>,
|
||||
|
||||
#[not_covariant] // avoids extra codegen, harmless to mark it as such
|
||||
dependent: Dependent,
|
||||
}
|
||||
// Boxes the iterator internally
|
||||
#[borrows(set)]
|
||||
#[covariant]
|
||||
iter: indexmap::set::Iter<'this, NodeId>,
|
||||
}
|
||||
|
||||
/// Due to the limitations of self-referencing, we cannot borrow the
|
||||
/// stack and iter simultaneously within the closure or the loop,
|
||||
/// Due to the limitations of ouroboros, we cannot borrow the
|
||||
/// stack and iter simultaneously, or directly within the loop,
|
||||
/// therefore this must be used to command the outside scope
|
||||
/// of what to do.
|
||||
enum IterResult<'a> {
|
||||
@@ -251,7 +251,7 @@ impl Runtime {
|
||||
}
|
||||
|
||||
while let Some(iter) = stack.last_mut() {
|
||||
let res = iter.with_dependent_mut(|_, iter| {
|
||||
let res = iter.with_iter_mut(|iter| {
|
||||
let Some(mut child) = iter.next().copied() else {
|
||||
return IterResult::Empty;
|
||||
};
|
||||
|
||||
@@ -83,7 +83,7 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
/// Takes a memoized, read-only slice of a signal. This is equivalent to the
|
||||
/// Takes a memoized, read-only slice of a signal. This is equivalent to the
|
||||
/// read-only half of [`create_slice`].
|
||||
pub fn create_read_slice<T, O>(
|
||||
cx: Scope,
|
||||
|
||||
@@ -39,8 +39,7 @@ impl<T> Clone for StoredValue<T> {
|
||||
impl<T> Copy for StoredValue<T> {}
|
||||
|
||||
impl<T> StoredValue<T> {
|
||||
/// Returns a clone of the signals current value, subscribing the effect
|
||||
/// to this signal.
|
||||
/// Returns a clone of the current stored value.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to access a value stored in a [`Scope`] that has been disposed.
|
||||
@@ -70,7 +69,7 @@ impl<T> StoredValue<T> {
|
||||
self.try_get_value().expect("could not get stored value")
|
||||
}
|
||||
|
||||
/// Same as [`StoredValue::get`] but will not panic by default.
|
||||
/// Same as [`StoredValue::get_value`] but will not panic by default.
|
||||
#[track_caller]
|
||||
pub fn try_get_value(&self) -> Option<T>
|
||||
where
|
||||
@@ -79,7 +78,7 @@ impl<T> StoredValue<T> {
|
||||
self.try_with_value(T::clone)
|
||||
}
|
||||
|
||||
/// Applies a function to the current stored value.
|
||||
/// Applies a function to the current stored value and returns the result.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to access a value stored in a [`Scope`] that has been disposed.
|
||||
@@ -105,8 +104,8 @@ impl<T> StoredValue<T> {
|
||||
self.try_with_value(f).expect("could not get stored value")
|
||||
}
|
||||
|
||||
/// Same as [`StoredValue::with`] but returns [`Some(O)]` only if
|
||||
/// the signal is still valid. [`None`] otherwise.
|
||||
/// Same as [`StoredValue::with_value`] but returns [`Some(O)]` only if
|
||||
/// the stored value has not yet been disposed. [`None`] otherwise.
|
||||
pub fn try_with_value<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
let value = {
|
||||
@@ -161,8 +160,8 @@ impl<T> StoredValue<T> {
|
||||
.expect("could not set stored value");
|
||||
}
|
||||
|
||||
/// Same as [`Self::update`], but returns [`Some(O)`] if the
|
||||
/// signal is still valid, [`None`] otherwise.
|
||||
/// Same as [`Self::update_value`], but returns [`Some(O)`] if the
|
||||
/// stored value has not yet been disposed, [`None`] otherwise.
|
||||
pub fn try_update_value<O>(self, f: impl FnOnce(&mut T) -> O) -> Option<O> {
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
let values = runtime.stored_values.borrow();
|
||||
@@ -195,8 +194,8 @@ impl<T> StoredValue<T> {
|
||||
self.try_set_value(value);
|
||||
}
|
||||
|
||||
/// Same as [`Self::set`], but returns [`None`] if the signal is
|
||||
/// still valid, [`Some(T)`] otherwise.
|
||||
/// Same as [`Self::set_value`], but returns [`None`] if the
|
||||
/// stored value has not yet been disposed, [`Some(T)`] otherwise.
|
||||
pub fn try_set_value(&self, value: T) -> Option<T> {
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
let values = runtime.stored_values.borrow();
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{
|
||||
/// Reactive Trigger, notifies reactive code to rerun.
|
||||
///
|
||||
/// See [`create_trigger`] for more.
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Trigger {
|
||||
pub(crate) runtime: RuntimeId,
|
||||
pub(crate) id: NodeId,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.3.1"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.3.1"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -95,8 +95,63 @@ where
|
||||
let action = use_resolved_path(cx, move || action.clone())
|
||||
.get()
|
||||
.unwrap_or_default();
|
||||
// multipart POST (setting Context-Type breaks the request)
|
||||
if method == "post" && enctype == "multipart/form-data" {
|
||||
ev.prevent_default();
|
||||
ev.stop_propagation();
|
||||
|
||||
let on_response = on_response.clone();
|
||||
spawn_local(async move {
|
||||
let res = gloo_net::http::Request::post(&action)
|
||||
.header("Accept", "application/json")
|
||||
.redirect(RequestRedirect::Follow)
|
||||
.body(form_data)
|
||||
.send()
|
||||
.await;
|
||||
match res {
|
||||
Err(e) => {
|
||||
error!("<Form/> error while POSTing: {e:#?}");
|
||||
if let Some(error) = error {
|
||||
error.set(Some(Box::new(e)));
|
||||
}
|
||||
}
|
||||
Ok(resp) => {
|
||||
if let Some(version) = action_version {
|
||||
version.update(|n| *n += 1);
|
||||
}
|
||||
if let Some(error) = error {
|
||||
error.set(None);
|
||||
}
|
||||
if let Some(on_response) = on_response.clone() {
|
||||
on_response(resp.as_raw());
|
||||
}
|
||||
// Check all the logical 3xx responses that might
|
||||
// get returned from a server function
|
||||
if resp.redirected() {
|
||||
let resp_url = &resp.url();
|
||||
match Url::try_from(resp_url.as_str()) {
|
||||
Ok(url) => {
|
||||
request_animation_frame(move || {
|
||||
if let Err(e) = navigate(
|
||||
&format!(
|
||||
"{}{}",
|
||||
url.pathname, url.search,
|
||||
),
|
||||
Default::default(),
|
||||
) {
|
||||
warn!("{}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => warn!("{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// POST
|
||||
if method == "post" {
|
||||
else if method == "post" {
|
||||
ev.prevent_default();
|
||||
ev.stop_propagation();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user