mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 16:54:41 -05:00
Compare commits
24 Commits
gbj-patch-
...
533
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99139ac505 | ||
|
|
2e2d500610 | ||
|
|
694e94db97 | ||
|
|
7e0bf3514f | ||
|
|
010bd8c05e | ||
|
|
4d3dd7a6e6 | ||
|
|
cc68d20758 | ||
|
|
20682e63ef | ||
|
|
40363df4a1 | ||
|
|
e3ea889d5f | ||
|
|
7f14da3026 | ||
|
|
06d28f7d67 | ||
|
|
27f2a672ba | ||
|
|
23f9d537e9 | ||
|
|
d86339bae3 | ||
|
|
846c338491 | ||
|
|
2d418dae93 | ||
|
|
91e0fcdc1b | ||
|
|
a9ed8461d1 | ||
|
|
5a71ca797a | ||
|
|
70eb07d7d6 | ||
|
|
71ee69af01 | ||
|
|
dd41c0586c | ||
|
|
aaf63dbf5c |
@@ -37,11 +37,18 @@ To do this, create a file in your project at `.cargo/config.toml`
|
||||
|
||||
```toml
|
||||
[unstable]
|
||||
[target.'cfg(target_arch = "wasm32")']
|
||||
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
|
||||
|
||||
@@ -90,7 +90,8 @@ view! { cx,
|
||||
<button
|
||||
// define an event listener with on:
|
||||
on:click=move |_| {
|
||||
set_count.update(|n| *n += 1);
|
||||
// on stable, this is set_count.set(3);
|
||||
set_count(3);
|
||||
}
|
||||
>
|
||||
// text nodes are wrapped in quotation marks
|
||||
@@ -142,6 +143,16 @@ in a function, telling the framework to update the view every time `count` chang
|
||||
`{count()}` access the value of `count` once, and passes an `i32` into the view,
|
||||
rendering it once, unreactively. You can see the difference in the CodeSandbox below!
|
||||
|
||||
Let’s make one final change. `set_count(3)` is a pretty useless thing for a click handler to do. Let’s replacing “set this value to 3” with “increment this value by 1”:
|
||||
|
||||
```rust
|
||||
move |_| {
|
||||
set_count.update(|n| *n += 1);
|
||||
}
|
||||
```
|
||||
|
||||
You can see here that while `set_count` just sets the value, `set_count.update()` gives us a mutable reference and mutates the value in place. Either one will trigger a reactive update in our UI.
|
||||
|
||||
> Throughout this tutorial, we’ll use CodeSandbox to show interactive examples. To
|
||||
> show the browser in the sandbox, you may need to click `Add DevTools >
|
||||
Other Previews > 8080.` Hover over any of the variables to show Rust-Analyzer details
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[tasks.web-test]
|
||||
dependencies = ["cargo-leptos-e2e"]
|
||||
dependencies = ["auto-setup", "cargo-leptos-e2e"]
|
||||
|
||||
[tasks.clean-all]
|
||||
dependencies = ["clean-cargo", "clean-node_modules", "clean-playwright"]
|
||||
|
||||
@@ -78,3 +78,10 @@ script = '''
|
||||
exit 1
|
||||
fi
|
||||
'''
|
||||
|
||||
[tasks.auto-setup]
|
||||
script = '''
|
||||
if [ ! -d "${END2END_DIR}/node_modules" ]; then
|
||||
cargo make setup
|
||||
fi
|
||||
'''
|
||||
|
||||
@@ -10,12 +10,10 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use axum::response::Response as AxumResponse;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use std::sync::Arc;
|
||||
use leptos::{LeptosOptions, view};
|
||||
use crate::landing::App;
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
let options = &*options;
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
|
||||
@@ -14,18 +14,17 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use errors_axum::*;
|
||||
use leptos::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use std::sync::Arc;
|
||||
}}
|
||||
|
||||
//Define a handler to test extractor with state
|
||||
#[cfg(feature = "ssr")]
|
||||
async fn custom_handler(
|
||||
Path(id): Path<String>,
|
||||
State(options): State<Arc<LeptosOptions>>,
|
||||
State(options): State<LeptosOptions>,
|
||||
req: Request<AxumBody>,
|
||||
) -> Response {
|
||||
let handler = leptos_axum::render_app_to_stream_with_context(
|
||||
(*options).clone(),
|
||||
options.clone(),
|
||||
move |cx| {
|
||||
provide_context(cx, id.clone());
|
||||
},
|
||||
@@ -44,7 +43,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 = Arc::new(conf.leptos_options);
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
|
||||
|
||||
@@ -53,7 +52,7 @@ async fn main() {
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
.route("/special/:id", get(custom_handler))
|
||||
.leptos_routes(
|
||||
leptos_options.clone(),
|
||||
&leptos_options,
|
||||
routes,
|
||||
|cx| view! { cx, <App/> },
|
||||
)
|
||||
|
||||
@@ -17,7 +17,9 @@ pub enum FetchError {
|
||||
Json,
|
||||
}
|
||||
|
||||
async fn fetch_cats(count: u32) -> Result<Vec<String>, FetchError> {
|
||||
type CatCount = usize;
|
||||
|
||||
async fn fetch_cats(count: CatCount) -> Result<Vec<String>, FetchError> {
|
||||
if count > 0 {
|
||||
// make the request
|
||||
let res = reqwasm::http::Request::get(&format!(
|
||||
@@ -32,6 +34,7 @@ async fn fetch_cats(count: u32) -> Result<Vec<String>, FetchError> {
|
||||
.map_err(|_| FetchError::Json)?
|
||||
// extract the URL field for each cat
|
||||
.into_iter()
|
||||
.take(count)
|
||||
.map(|cat| cat.url)
|
||||
.collect::<Vec<_>>();
|
||||
Ok(res)
|
||||
@@ -41,7 +44,7 @@ async fn fetch_cats(count: u32) -> Result<Vec<String>, FetchError> {
|
||||
}
|
||||
|
||||
pub fn fetch_example(cx: Scope) -> impl IntoView {
|
||||
let (cat_count, set_cat_count) = create_signal::<u32>(cx, 0);
|
||||
let (cat_count, set_cat_count) = create_signal::<CatCount>(cx, 0);
|
||||
|
||||
// we use local_resource here because
|
||||
// 1) our error type isn't serializable/deserializable
|
||||
@@ -75,7 +78,7 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
|
||||
cats.read(cx).map(|data| {
|
||||
data.map(|data| {
|
||||
data.iter()
|
||||
.map(|s| view! { cx, <span>{s}</span> })
|
||||
.map(|s| view! { cx, <p><img src={s}/></p> })
|
||||
.collect_view(cx)
|
||||
})
|
||||
})
|
||||
@@ -89,7 +92,7 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
|
||||
type="number"
|
||||
prop:value=move || cat_count.get().to_string()
|
||||
on:input=move |ev| {
|
||||
let val = event_target_value(&ev).parse::<u32>().unwrap_or(0);
|
||||
let val = event_target_value(&ev).parse::<CatCount>().unwrap_or(0);
|
||||
set_cat_count(val);
|
||||
}
|
||||
/>
|
||||
@@ -98,7 +101,9 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
|
||||
<Transition fallback=move || {
|
||||
view! { cx, <div>"Loading (Suspense Fallback)..."</div> }
|
||||
}>
|
||||
<div>
|
||||
{cats_view}
|
||||
</div>
|
||||
</Transition>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::{component, view, IntoView, Scope};
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
mod api;
|
||||
@@ -9,23 +9,25 @@ use routes::{nav::*, stories::*, story::*, users::*};
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
provide_meta_context(cx);
|
||||
view! {
|
||||
cx,
|
||||
<>
|
||||
<Stylesheet id="leptos" href="/pkg/hackernews.css"/>
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
|
||||
<Router>
|
||||
<Nav />
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="users/:id" view=|cx| view! { cx, <User/> }/>
|
||||
<Route path="stories/:id" view=|cx| view! { cx, <Story/> }/>
|
||||
<Route path=":stories?" view=|cx| view! { cx, <Stories/> }/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
</>
|
||||
let (is_routing, set_is_routing) = create_signal(cx, false);
|
||||
|
||||
view! { cx,
|
||||
<Stylesheet id="leptos" href="/pkg/hackernews.css"/>
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
|
||||
// adding `set_is_routing` causes the router to wait for async data to load on new pages
|
||||
<Router set_is_routing>
|
||||
// shows a progress bar while async data are loading
|
||||
<RoutingProgress is_routing max_time=std::time::Duration::from_millis(250)/>
|
||||
<Nav />
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="users/:id" view=|cx| view! { cx, <User/> }/>
|
||||
<Route path="stories/:id" view=|cx| view! { cx, <Story/> }/>
|
||||
<Route path=":stories?" view=|cx| view! { cx, <Stories/> }/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,12 +11,10 @@ if #[cfg(feature = "ssr")] {
|
||||
use axum::response::Response as AxumResponse;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use std::sync::Arc;
|
||||
use leptos::{LeptosOptions};
|
||||
use crate::error_template::error_template;
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
let options = &*options;
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ if #[cfg(feature = "ssr")] {
|
||||
routing::get,
|
||||
};
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use std::sync::Arc;
|
||||
use hackernews_axum::fallback::file_and_error_handler;
|
||||
|
||||
#[tokio::main]
|
||||
@@ -17,7 +16,7 @@ if #[cfg(feature = "ssr")] {
|
||||
use hackernews_axum::*;
|
||||
|
||||
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
|
||||
let leptos_options = Arc::new(conf.leptos_options);
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
|
||||
|
||||
@@ -26,7 +25,7 @@ if #[cfg(feature = "ssr")] {
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/favicon.ico", get(file_and_error_handler))
|
||||
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> } )
|
||||
.leptos_routes(&leptos_options, routes, |cx| view! { cx, <App/> } )
|
||||
.fallback(file_and_error_handler)
|
||||
.with_state(leptos_options);
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
axum = { version = "0.6.1", optional = true }
|
||||
axum = { version = "0.6.1", optional = true, features=["macros"] }
|
||||
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 }
|
||||
@@ -33,10 +33,10 @@ sqlx = { version = "0.6.2", features = [
|
||||
], optional = true }
|
||||
thiserror = "1.0.38"
|
||||
wasm-bindgen = "0.2"
|
||||
axum_sessions_auth = { version = "7.0.0", features = [
|
||||
axum_session_auth = { version = "0.2.1", features = [
|
||||
"sqlite-rustls",
|
||||
], optional = true }
|
||||
axum_database_sessions = { version = "7.0.0", features = [
|
||||
axum_session = { version = "0.2.3", features = [
|
||||
"sqlite-rustls",
|
||||
], optional = true }
|
||||
bcrypt = { version = "0.14", optional = true }
|
||||
@@ -49,8 +49,8 @@ ssr = [
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:tokio",
|
||||
"dep:axum_sessions_auth",
|
||||
"dep:axum_database_sessions",
|
||||
"dep:axum_session_auth",
|
||||
"dep:axum_session",
|
||||
"dep:async-trait",
|
||||
"dep:sqlx",
|
||||
"dep:bcrypt",
|
||||
|
||||
@@ -6,10 +6,10 @@ use std::collections::HashSet;
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use sqlx::SqlitePool;
|
||||
use axum_sessions_auth::{SessionSqlitePool, Authentication, HasPermission};
|
||||
use axum_session_auth::{SessionSqlitePool, Authentication, HasPermission};
|
||||
use bcrypt::{hash, verify, DEFAULT_COST};
|
||||
use crate::todo::{pool, auth};
|
||||
pub type AuthSession = axum_sessions_auth::AuthSession<User, i64, SessionSqlitePool, SqlitePool>;
|
||||
pub type AuthSession = axum_session_auth::AuthSession<User, i64, SessionSqlitePool, SqlitePool>;
|
||||
}}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
||||
@@ -11,13 +11,11 @@ if #[cfg(feature = "ssr")] {
|
||||
use axum::response::Response as AxumResponse;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use std::sync::Arc;
|
||||
use leptos::{LeptosOptions, Errors, view};
|
||||
use crate::error_template::ErrorTemplate;
|
||||
use crate::errors::TodoAppError;
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
let options = &*options;
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ pub mod auth;
|
||||
pub mod error_template;
|
||||
pub mod errors;
|
||||
pub mod fallback;
|
||||
pub mod state;
|
||||
pub mod todo;
|
||||
|
||||
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
|
||||
|
||||
@@ -6,38 +6,38 @@ if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
response::{Response, IntoResponse},
|
||||
routing::get,
|
||||
extract::{Path, State, Extension, RawQuery},
|
||||
extract::{Path, State, RawQuery},
|
||||
http::{Request, header::HeaderMap},
|
||||
body::Body as AxumBody,
|
||||
Router,
|
||||
};
|
||||
use session_auth_axum::todo::*;
|
||||
use session_auth_axum::auth::*;
|
||||
use session_auth_axum::state::AppState;
|
||||
use session_auth_axum::*;
|
||||
use session_auth_axum::fallback::file_and_error_handler;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes, handle_server_fns_with_context};
|
||||
use leptos::{log, view, provide_context, LeptosOptions, get_configuration};
|
||||
use std::sync::Arc;
|
||||
use leptos::{log, view, provide_context, get_configuration};
|
||||
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions};
|
||||
use axum_database_sessions::{SessionConfig, SessionLayer, SessionStore};
|
||||
use axum_sessions_auth::{AuthSessionLayer, AuthConfig, SessionSqlitePool};
|
||||
use axum_session::{SessionConfig, SessionLayer, SessionStore};
|
||||
use axum_session_auth::{AuthSessionLayer, AuthConfig, SessionSqlitePool};
|
||||
|
||||
async fn server_fn_handler(Extension(pool): Extension<SqlitePool>, auth_session: AuthSession, path: Path<String>, headers: HeaderMap, raw_query: RawQuery,
|
||||
async fn server_fn_handler(State(app_state): State<AppState>, auth_session: AuthSession, path: Path<String>, headers: HeaderMap, raw_query: RawQuery,
|
||||
request: Request<AxumBody>) -> impl IntoResponse {
|
||||
|
||||
log!("{:?}", path);
|
||||
|
||||
handle_server_fns_with_context(path, headers, raw_query, move |cx| {
|
||||
provide_context(cx, auth_session.clone());
|
||||
provide_context(cx, pool.clone());
|
||||
provide_context(cx, app_state.pool.clone());
|
||||
}, request).await
|
||||
}
|
||||
|
||||
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(),
|
||||
async fn leptos_routes_handler(auth_session: AuthSession, State(app_state): State<AppState>, req: Request<AxumBody>) -> Response{
|
||||
let handler = leptos_axum::render_app_to_stream_with_context(app_state.leptos_options.clone(),
|
||||
move |cx| {
|
||||
provide_context(cx, auth_session.clone());
|
||||
provide_context(cx, pool.clone());
|
||||
provide_context(cx, app_state.pool.clone());
|
||||
},
|
||||
|cx| view! { cx, <TodoApp/> }
|
||||
);
|
||||
@@ -68,20 +68,24 @@ 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 = Arc::new(conf.leptos_options);
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(|cx| view! { cx, <TodoApp/> }).await;
|
||||
|
||||
let app_state = AppState{
|
||||
leptos_options,
|
||||
pool: pool.clone(),
|
||||
};
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name", get(server_fn_handler).post(server_fn_handler))
|
||||
.leptos_routes_with_handler(routes, get(leptos_routes_handler) )
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(Some(pool.clone()))
|
||||
.with_config(auth_config))
|
||||
.with_config(auth_config))
|
||||
.layer(SessionLayer::new(session_store))
|
||||
.layer(Extension(pool))
|
||||
.with_state(leptos_options);
|
||||
.with_state(app_state);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
|
||||
17
examples/session_auth_axum/src/state.rs
Normal file
17
examples/session_auth_axum/src/state.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use leptos::LeptosOptions;
|
||||
use sqlx::SqlitePool;
|
||||
use axum::extract::FromRef;
|
||||
|
||||
/// This takes advantage of Axum's SubStates feature by deriving FromRef. This is the only way to have more than one
|
||||
/// item in Axum's State. Leptos requires you to have leptosOptions in your State struct for the leptos route handlers
|
||||
#[derive(FromRef, Debug, Clone)]
|
||||
pub struct AppState{
|
||||
pub leptos_options: LeptosOptions,
|
||||
pub pool: SqlitePool
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,8 +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, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
let options = &*options;
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
|
||||
@@ -5,11 +5,10 @@ async fn main() {
|
||||
use leptos::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use ssr_modes_axum::{app::*, fallback::file_and_error_handler};
|
||||
use std::sync::Arc;
|
||||
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let leptos_options = Arc::new(conf.leptos_options);
|
||||
let leptos_options = conf.leptos_options;
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
|
||||
|
||||
@@ -19,7 +18,7 @@ async fn main() {
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
.leptos_routes(
|
||||
leptos_options.clone(),
|
||||
&leptos_options,
|
||||
routes,
|
||||
|cx| view! { cx, <App/> },
|
||||
)
|
||||
|
||||
@@ -11,13 +11,11 @@ if #[cfg(feature = "ssr")] {
|
||||
use axum::response::Response as AxumResponse;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use std::sync::Arc;
|
||||
use leptos::{LeptosOptions, Errors, view};
|
||||
use crate::error_template::ErrorTemplate;
|
||||
use crate::errors::TodoAppError;
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
let options = &*options;
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
|
||||
@@ -15,11 +15,10 @@ cfg_if! {
|
||||
use todo_app_sqlite_axum::*;
|
||||
use crate::fallback::file_and_error_handler;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use std::sync::Arc;
|
||||
|
||||
//Define a handler to test extractor with state
|
||||
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(),
|
||||
async fn custom_handler(Path(id): Path<String>, State(options): State<LeptosOptions>, req: Request<AxumBody>) -> Response{
|
||||
let handler = leptos_axum::render_app_to_stream_with_context(options,
|
||||
move |cx| {
|
||||
provide_context(cx, id.clone());
|
||||
},
|
||||
@@ -42,7 +41,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 = Arc::new(conf.leptos_options);
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(|cx| view! { cx, <TodoApp/> }).await;
|
||||
|
||||
@@ -50,7 +49,7 @@ cfg_if! {
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
.route("/special/:id", get(custom_handler))
|
||||
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <TodoApp/> } )
|
||||
.leptos_routes(&leptos_options, routes, |cx| view! { cx, <TodoApp/> } )
|
||||
.fallback(file_and_error_handler)
|
||||
.with_state(leptos_options);
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ cfg_if! {
|
||||
_ = GetTodos::register();
|
||||
_ = AddTodo::register();
|
||||
_ = DeleteTodo::register();
|
||||
_ = FormDataHandler::register();
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
|
||||
@@ -108,30 +107,6 @@ pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct FormData {
|
||||
hi: String,
|
||||
}
|
||||
|
||||
#[server(FormDataHandler, "/api")]
|
||||
pub async fn form_data(cx: Scope) -> Result<FormData, ServerFnError> {
|
||||
use axum::extract::FromRequest;
|
||||
|
||||
let req = use_context::<leptos_axum::LeptosRequest<axum::body::Body>>(cx)
|
||||
.and_then(|req| req.take_request())
|
||||
.unwrap();
|
||||
if req.method() == http::Method::POST {
|
||||
let form = axum::Form::from_request(req, &())
|
||||
.await
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
|
||||
Ok(form.0)
|
||||
} else {
|
||||
Err(ServerFnError::ServerError(
|
||||
"wrong form fields submitted".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
//let id = use_context::<String>(cx);
|
||||
@@ -148,24 +123,7 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
<Routes>
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<Todos/>
|
||||
}/> //Route
|
||||
<Route path="weird" methods=&[Method::Get, Method::Post]
|
||||
ssr=SsrMode::Async
|
||||
view=|cx| {
|
||||
let res = create_resource(cx, || (), move |_| async move {
|
||||
form_data(cx).await
|
||||
});
|
||||
view! { cx,
|
||||
<Suspense fallback=|| ()>
|
||||
<pre>
|
||||
{move || {
|
||||
res.with(cx, |body| format!("{body:#?}"))
|
||||
}}
|
||||
</pre>
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
/>
|
||||
}/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
@@ -7,8 +7,8 @@ if #[cfg(feature = "ssr")] {
|
||||
errors::TodoAppError,
|
||||
};
|
||||
use http::Uri;
|
||||
use leptos::{view, Errors, LeptosOptions};
|
||||
use std::sync::Arc;
|
||||
use leptos::{view, Errors, LeptosOptions};
|
||||
use viz::{
|
||||
handlers::serve, header::HeaderMap, types::RouteInfo, Body, Error, Handler,
|
||||
Request, RequestExt, Response, ResponseExt, Result,
|
||||
@@ -18,7 +18,7 @@ if #[cfg(feature = "ssr")] {
|
||||
let uri = req.uri().clone();
|
||||
let headers = req.headers().clone();
|
||||
let route_info = req.route_info().clone();
|
||||
let options = &*req.state::<Arc<LeptosOptions>>().ok_or(
|
||||
let options = req.state::<LeptosOptions>().ok_or(
|
||||
Error::Responder(Response::text("missing state type LeptosOptions")),
|
||||
)?;
|
||||
let root = &options.site_root;
|
||||
|
||||
@@ -7,7 +7,6 @@ cfg_if! {
|
||||
use crate::fallback::file_and_error_handler;
|
||||
use crate::todo::*;
|
||||
use leptos_viz::{generate_route_list, LeptosRoutes};
|
||||
use std::sync::Arc;
|
||||
use todo_app_sqlite_viz::*;
|
||||
use viz::{
|
||||
types::{State, StateError},
|
||||
@@ -17,8 +16,8 @@ cfg_if! {
|
||||
//Define a handler to test extractor with state
|
||||
async fn custom_handler(req: Request) -> Result<Response> {
|
||||
let id = req.params::<String>()?;
|
||||
let options = &*req
|
||||
.state::<Arc<LeptosOptions>>()
|
||||
let options = req
|
||||
.state::<LeptosOptions>()
|
||||
.ok_or(StateError::new::<LeptosOptions>())?;
|
||||
let handler = leptos_viz::render_app_to_stream_with_context(
|
||||
options.clone(),
|
||||
@@ -59,7 +58,7 @@ cfg_if! {
|
||||
|cx| view! { cx, <TodoApp/> },
|
||||
)
|
||||
.get("/*", file_and_error_handler)
|
||||
.with(State(Arc::new(leptos_options)));
|
||||
.with(State(leptos_options));
|
||||
|
||||
// run our app with hyper
|
||||
// `viz::Server` is a re-export of `hyper::Server`
|
||||
|
||||
@@ -228,7 +228,7 @@ pub fn handle_server_fns_with_context(
|
||||
use_context::<ResponseOptions>(cx).unwrap();
|
||||
|
||||
let mut res: HttpResponseBuilder;
|
||||
let mut res_parts = res_options.0.write();
|
||||
let res_parts = res_options.0.write();
|
||||
|
||||
if accept_header == Some("application/json")
|
||||
|| accept_header
|
||||
@@ -256,11 +256,10 @@ pub fn handle_server_fns_with_context(
|
||||
// Use provided ResponseParts headers if they exist
|
||||
let _count = res_parts
|
||||
.headers
|
||||
.drain()
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(k, v)| {
|
||||
if let Some(k) = k {
|
||||
res.append_header((k, v));
|
||||
}
|
||||
res.append_header((k, v));
|
||||
})
|
||||
.count();
|
||||
|
||||
@@ -807,8 +806,7 @@ async fn build_stream_response(
|
||||
|
||||
let res_options = res_options.0.read();
|
||||
|
||||
let (status, mut headers) =
|
||||
(res_options.status, res_options.headers.clone());
|
||||
let (status, headers) = (res_options.status, res_options.headers.clone());
|
||||
let status = status.unwrap_or_default();
|
||||
|
||||
let complete_stream =
|
||||
@@ -817,12 +815,12 @@ async fn build_stream_response(
|
||||
let mut res = HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.streaming(complete_stream);
|
||||
|
||||
// Add headers manipulated in the response
|
||||
for (key, value) in headers.drain() {
|
||||
if let Some(key) = key {
|
||||
res.headers_mut().append(key, value);
|
||||
}
|
||||
for (key, value) in headers.into_iter() {
|
||||
res.headers_mut().append(key, value);
|
||||
}
|
||||
|
||||
// Set status to what is returned in the function
|
||||
let res_status = res.status_mut();
|
||||
*res_status = status;
|
||||
@@ -847,18 +845,16 @@ async fn render_app_async_helper(
|
||||
|
||||
let res_options = res_options.0.read();
|
||||
|
||||
let (status, mut headers) =
|
||||
(res_options.status, res_options.headers.clone());
|
||||
let (status, headers) = (res_options.status, res_options.headers.clone());
|
||||
let status = status.unwrap_or_default();
|
||||
|
||||
let mut res = HttpResponse::Ok().content_type("text/html").body(html);
|
||||
|
||||
// Add headers manipulated in the response
|
||||
for (key, value) in headers.drain() {
|
||||
if let Some(key) = key {
|
||||
res.headers_mut().append(key, value);
|
||||
}
|
||||
for (key, value) in headers.into_iter() {
|
||||
res.headers_mut().append(key, value);
|
||||
}
|
||||
|
||||
// Set status to what is returned in the function
|
||||
let res_status = res.status_mut();
|
||||
*res_status = status;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
use axum::{
|
||||
body::{Body, Bytes, Full, StreamBody},
|
||||
extract::{Path, RawQuery},
|
||||
extract::{FromRef, FromRequestParts, Path, RawQuery},
|
||||
http::{
|
||||
header::{HeaderName, HeaderValue},
|
||||
HeaderMap, Request, StatusCode,
|
||||
@@ -150,69 +150,6 @@ pub async fn generate_request_and_parts(
|
||||
(request, request_parts)
|
||||
}
|
||||
|
||||
/// A struct to hold the [`http::request::Request`] and allow users to take ownership of it
|
||||
/// Required by `Request` not being `Clone`. See
|
||||
/// [this issue](https://github.com/hyperium/http/pull/574) for eventual resolution:
|
||||
#[derive(Debug, Default)]
|
||||
pub struct LeptosRequest<B>(Arc<RwLock<Option<Request<B>>>>);
|
||||
|
||||
impl<B> Clone for LeptosRequest<B> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
impl<B> LeptosRequest<B> {
|
||||
/// Overwrite the contents of a LeptosRequest with a new `Request<B>`
|
||||
pub fn overwrite(&self, req: Option<Request<B>>) {
|
||||
let mut writable = self.0.write();
|
||||
*writable = req
|
||||
}
|
||||
/// Consume the inner `Request<B>` inside the LeptosRequest and return it
|
||||
///```rust, ignore
|
||||
/// use axum::{
|
||||
/// RequestPartsExt,
|
||||
/// headers::Host
|
||||
/// };
|
||||
/// #[server(GetHost, "/api")]
|
||||
/// pub async fn get_host(cx: Scope) -> Result((), ServerFnError){
|
||||
/// let req = use_context::<leptos_axum::LeptosRequest<axum::body::Body>>(cx);
|
||||
/// if let Some(req) = req{
|
||||
/// let owned_req = req.take_request().unwrap();
|
||||
/// let (mut parts, _body) = owned_req.into_parts();
|
||||
/// let host: TypedHeader<Host> = parts.extract().await().unwrap();
|
||||
/// println!("Host: {host:#?}");
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn take_request(&self) -> Option<Request<B>> {
|
||||
let mut writable = self.0.write();
|
||||
writable.take()
|
||||
}
|
||||
/// Can be used to get immutable access to the interior fields of Request
|
||||
/// and do something with them
|
||||
pub fn with(&self, with_fn: impl Fn(Option<&Request<B>>)) {
|
||||
let readable = self.0.read();
|
||||
with_fn(readable.as_ref());
|
||||
}
|
||||
|
||||
/// Can be used to mutate the fields of the Request
|
||||
pub fn update(&self, update_fn: impl Fn(Option<&mut Request<B>>)) {
|
||||
let mut writable = self.0.write();
|
||||
update_fn(writable.as_mut());
|
||||
}
|
||||
}
|
||||
/// Generate a wrapper for the http::Request::Request type that allows one to
|
||||
/// process it, access the body, and use axum Extractors on it.
|
||||
/// Required by Request not being Clone. See
|
||||
/// [this issue](https://github.com/hyperium/http/pull/574) for eventual resolution:
|
||||
pub async fn generate_leptos_request<B>(req: Request<B>) -> LeptosRequest<B>
|
||||
where
|
||||
B: Default + std::fmt::Debug,
|
||||
{
|
||||
let leptos_request = LeptosRequest::default();
|
||||
leptos_request.overwrite(Some(req));
|
||||
leptos_request
|
||||
}
|
||||
/// An Axum handlers to listens for a request with Leptos server function arguments in the body,
|
||||
/// run the server function if found, and return the resulting [Response].
|
||||
///
|
||||
@@ -310,9 +247,8 @@ async fn handle_server_fns_inner(
|
||||
additional_context(cx);
|
||||
|
||||
let (req, req_parts) = generate_request_and_parts(req).await;
|
||||
let leptos_req = generate_leptos_request(req).await; // Add this so we can get details about the Request
|
||||
provide_context(cx, req_parts.clone());
|
||||
provide_context(cx, leptos_req);
|
||||
provide_context(cx, ExtractorHelper::from(req));
|
||||
// Add this so that we can set headers and status of the response
|
||||
provide_context(cx, ResponseOptions::default());
|
||||
|
||||
@@ -675,9 +611,8 @@ where
|
||||
|
||||
let full_path = format!("http://leptos.dev{path}");
|
||||
let (req, req_parts) = generate_request_and_parts(req).await;
|
||||
let leptos_req = generate_leptos_request(req).await;
|
||||
move |cx| {
|
||||
provide_contexts(cx, full_path, req_parts,leptos_req, default_res_options);
|
||||
provide_contexts(cx, full_path, req_parts, req.into(), default_res_options);
|
||||
app_fn(cx).into_view(cx)
|
||||
}
|
||||
};
|
||||
@@ -829,9 +764,8 @@ where
|
||||
let app = {
|
||||
let full_path = full_path.clone();
|
||||
let (req, req_parts) = generate_request_and_parts(req).await;
|
||||
let leptos_req = generate_leptos_request(req).await;
|
||||
move |cx| {
|
||||
provide_contexts(cx, full_path, req_parts,leptos_req, default_res_options);
|
||||
provide_contexts(cx, full_path, req_parts, req.into(), default_res_options);
|
||||
app_fn(cx).into_view(cx)
|
||||
}
|
||||
};
|
||||
@@ -851,19 +785,20 @@ where
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
fn provide_contexts<B: 'static + std::fmt::Debug + std::default::Default>(
|
||||
fn provide_contexts(
|
||||
cx: Scope,
|
||||
path: String,
|
||||
req_parts: RequestParts,
|
||||
leptos_req: LeptosRequest<B>,
|
||||
extractor: ExtractorHelper,
|
||||
default_res_options: ResponseOptions,
|
||||
) {
|
||||
let integration = ServerIntegration { path };
|
||||
provide_context(cx, RouterIntegrationContext::new(integration));
|
||||
provide_context(cx, MetaContext::new());
|
||||
provide_context(cx, req_parts);
|
||||
provide_context(cx, leptos_req);
|
||||
provide_context(cx, extractor);
|
||||
provide_context(cx, default_res_options);
|
||||
provide_server_redirect(cx, move |path| redirect(cx, path));
|
||||
}
|
||||
@@ -999,9 +934,8 @@ where
|
||||
let app = {
|
||||
let full_path = full_path.clone();
|
||||
let (req, req_parts) = generate_request_and_parts(req).await;
|
||||
let leptos_req = generate_leptos_request(req).await;
|
||||
move |cx| {
|
||||
provide_contexts(cx, full_path, req_parts,leptos_req, default_res_options);
|
||||
provide_contexts(cx, full_path, req_parts, req.into(), default_res_options);
|
||||
app_fn(cx).into_view(cx)
|
||||
}
|
||||
};
|
||||
@@ -1126,39 +1060,16 @@ 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<OP>
|
||||
pub trait LeptosRoutes<S>
|
||||
where
|
||||
OP: LeptosOptionProvider,
|
||||
LeptosOptions: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
fn leptos_routes<IV>(
|
||||
self,
|
||||
options: OP,
|
||||
options: &S,
|
||||
paths: Vec<RouteListing>,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
) -> Self
|
||||
@@ -1167,7 +1078,7 @@ where
|
||||
|
||||
fn leptos_routes_with_context<IV>(
|
||||
self,
|
||||
options: OP,
|
||||
options: &S,
|
||||
paths: Vec<RouteListing>,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
@@ -1181,20 +1092,21 @@ where
|
||||
handler: H,
|
||||
) -> Self
|
||||
where
|
||||
H: axum::handler::Handler<T, OP, axum::body::Body>,
|
||||
H: axum::handler::Handler<T, S, 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<OP> LeptosRoutes<OP> for axum::Router<OP>
|
||||
impl<S> LeptosRoutes<S> for axum::Router<S>
|
||||
where
|
||||
OP: LeptosOptionProvider + Clone + Send + Sync + 'static,
|
||||
LeptosOptions: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
#[tracing::instrument(level = "info", fields(error), skip_all)]
|
||||
fn leptos_routes<IV>(
|
||||
self,
|
||||
options: OP,
|
||||
options: &S,
|
||||
paths: Vec<RouteListing>,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
) -> Self
|
||||
@@ -1207,7 +1119,7 @@ where
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
fn leptos_routes_with_context<IV>(
|
||||
self,
|
||||
options: OP,
|
||||
options: &S,
|
||||
paths: Vec<RouteListing>,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
@@ -1225,7 +1137,7 @@ where
|
||||
match listing.mode() {
|
||||
SsrMode::OutOfOrder => {
|
||||
let s = render_app_to_stream_with_context(
|
||||
options.options(),
|
||||
LeptosOptions::from_ref(options),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
@@ -1239,7 +1151,7 @@ where
|
||||
}
|
||||
SsrMode::PartiallyBlocked => {
|
||||
let s = render_app_to_stream_with_context_and_replace_blocks(
|
||||
options.options(),
|
||||
LeptosOptions::from_ref(options),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
true
|
||||
@@ -1254,7 +1166,7 @@ where
|
||||
}
|
||||
SsrMode::InOrder => {
|
||||
let s = render_app_to_stream_in_order_with_context(
|
||||
options.options(),
|
||||
LeptosOptions::from_ref(options),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
@@ -1268,7 +1180,7 @@ where
|
||||
}
|
||||
SsrMode::Async => {
|
||||
let s = render_app_async_with_context(
|
||||
options.options(),
|
||||
LeptosOptions::from_ref(options),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
@@ -1294,7 +1206,7 @@ where
|
||||
handler: H,
|
||||
) -> Self
|
||||
where
|
||||
H: axum::handler::Handler<T, OP, axum::body::Body>,
|
||||
H: axum::handler::Handler<T, S, axum::body::Body>,
|
||||
T: 'static,
|
||||
{
|
||||
let mut router = self;
|
||||
@@ -1329,3 +1241,111 @@ fn get_leptos_pool() -> LocalPoolHandle {
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ExtractorHelper {
|
||||
parts: Arc<tokio::sync::Mutex<Parts>>,
|
||||
}
|
||||
|
||||
impl ExtractorHelper {
|
||||
pub fn new(parts: Parts) -> Self {
|
||||
Self {
|
||||
parts: Arc::new(tokio::sync::Mutex::new(parts)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn extract<F, T, U>(&self, f: F) -> Result<U, T::Rejection>
|
||||
where
|
||||
F: Extractor<T, U>,
|
||||
T: std::fmt::Debug + Send + FromRequestParts<()> + 'static,
|
||||
T::Rejection: std::fmt::Debug + Send + 'static,
|
||||
{
|
||||
let mut parts = self.parts.lock().await;
|
||||
let data = T::from_request_parts(&mut parts, &()).await?;
|
||||
Ok(f.call(data).await)
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> From<Request<B>> for ExtractorHelper {
|
||||
fn from(req: Request<B>) -> Self {
|
||||
// TODO provide body for extractors there, too?
|
||||
let (parts, _) = req.into_parts();
|
||||
ExtractorHelper::new(parts)
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper to make it easier to use Axum extractors in server functions. This takes
|
||||
/// a handler function as its argument. The handler rules similar to Axum
|
||||
/// [handlers](https://docs.rs/axum/latest/axum/extract/index.html#intro): it is an async function
|
||||
/// whose arguments are “extractors.”
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[server(QueryExtract, "/api")]
|
||||
/// pub async fn query_extract(cx: Scope) -> Result<String, ServerFnError> {
|
||||
/// use axum::{extract::Query, http::Method};
|
||||
/// use leptos_axum::extract;
|
||||
///
|
||||
/// extract(cx, |method: Method, res: Query<MyQuery>| async move {
|
||||
/// format!("{method:?} and {}", res.q)
|
||||
/// },
|
||||
/// )
|
||||
/// .await
|
||||
/// .map_err(|e| ServerFnError::ServerError("Could not extract method and query...".to_string()))
|
||||
/// }
|
||||
/// ```
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub async fn extract<T, U>(
|
||||
cx: Scope,
|
||||
f: impl Extractor<T, U>,
|
||||
) -> Result<U, T::Rejection>
|
||||
where
|
||||
T: std::fmt::Debug + Send + FromRequestParts<()> + 'static,
|
||||
T::Rejection: std::fmt::Debug + Send + 'static,
|
||||
{
|
||||
use_context::<ExtractorHelper>(cx)
|
||||
.expect(
|
||||
"should have had ExtractorHelper provided by the leptos_axum \
|
||||
integration",
|
||||
)
|
||||
.extract(f)
|
||||
.await
|
||||
}
|
||||
|
||||
pub trait Extractor<T, U>
|
||||
where
|
||||
T: FromRequestParts<()>,
|
||||
{
|
||||
fn call(&self, args: T) -> Pin<Box<dyn Future<Output = U>>>;
|
||||
}
|
||||
|
||||
macro_rules! factory_tuple ({ $($param:ident)* } => {
|
||||
impl<Func, Fut, U, $($param,)*> Extractor<($($param,)*), U> for Func
|
||||
where
|
||||
$($param: FromRequestParts<()> + Send,)*
|
||||
Func: Fn($($param),*) -> Fut + 'static,
|
||||
Fut: Future<Output = U> + 'static,
|
||||
{
|
||||
#[inline]
|
||||
#[allow(non_snake_case)]
|
||||
fn call(&self, ($($param,)*): ($($param,)*)) -> Pin<Box<dyn Future<Output = U>>> {
|
||||
Box::pin((self)($($param,)*))
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
factory_tuple! { A }
|
||||
factory_tuple! { A B }
|
||||
factory_tuple! { A B C }
|
||||
factory_tuple! { A B C D }
|
||||
factory_tuple! { A B C D E }
|
||||
factory_tuple! { A B C D E F }
|
||||
factory_tuple! { A B C D E F G }
|
||||
factory_tuple! { A B C D E F G H }
|
||||
factory_tuple! { A B C D E F G H I }
|
||||
factory_tuple! { A B C D E F G H I J }
|
||||
factory_tuple! { A B C D E F G H I J K }
|
||||
factory_tuple! { A B C D E F G H I J K L }
|
||||
factory_tuple! { A B C D E F G H I J K L M }
|
||||
factory_tuple! { A B C D E F G H I J K L M N }
|
||||
factory_tuple! { A B C D E F G H I J K L M N O }
|
||||
factory_tuple! { A B C D E F G H I J K L M N O P }
|
||||
|
||||
66
leptos/src/await_.rs
Normal file
66
leptos/src/await_.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use crate::Suspense;
|
||||
use leptos_dom::IntoView;
|
||||
use leptos_macro::{component, view};
|
||||
use leptos_reactive::{
|
||||
create_blocking_resource, create_resource, store_value, Scope, Serializable,
|
||||
};
|
||||
|
||||
#[component]
|
||||
/// Allows you to inline the data loading for an `async` block or
|
||||
/// server function directly into your view. This is the equivalent of combining a
|
||||
/// [`create_resource`] that only loads once (i.e., with a source signal `|| ()`) with
|
||||
/// a [`Suspense`] with no `fallback`.
|
||||
/// ```
|
||||
/// # use leptos_reactive::*;
|
||||
/// # use leptos_macro::*;
|
||||
/// # use leptos_dom::*; use leptos::*;
|
||||
/// # if false {
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// async fn fetch_monkeys(monkey: i32) -> i32 {
|
||||
/// // do some expensive work
|
||||
/// 3
|
||||
/// }
|
||||
///
|
||||
/// view! { cx,
|
||||
/// <Await
|
||||
/// future=|cx| fetch_monkeys(3)
|
||||
/// view=|cx, data| {
|
||||
/// view! { cx, <p>{*data} " little monkeys, jumping on the bed."</p> }
|
||||
/// }
|
||||
/// />
|
||||
/// }
|
||||
/// # });
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn Await<T, Fut, FF, VF, V>(
|
||||
cx: Scope,
|
||||
/// A function that takes a [`Scope`] and returns the [`Future`](std::future::Future) that
|
||||
/// will the component will `.await` before rendering.
|
||||
future: FF,
|
||||
/// If `true`, the component will use [`create_blocking_resource`], preventing
|
||||
/// the HTML stream from returning anything before `future` has resolved.
|
||||
#[prop(optional)]
|
||||
blocking: bool,
|
||||
/// A function that takes a [`Scope`] and a reference to the resolved data from the `future`
|
||||
/// renders a view.
|
||||
view: VF,
|
||||
) -> impl IntoView
|
||||
where
|
||||
Fut: std::future::Future<Output = T> + 'static,
|
||||
FF: Fn(Scope) -> Fut + 'static,
|
||||
V: IntoView,
|
||||
VF: Fn(Scope, &T) -> V + 'static,
|
||||
T: Serializable + 'static,
|
||||
{
|
||||
let res = if blocking {
|
||||
create_blocking_resource(cx, || (), move |_| future(cx))
|
||||
} else {
|
||||
create_resource(cx, || (), move |_| future(cx))
|
||||
};
|
||||
let view = store_value(cx, view);
|
||||
view! { cx,
|
||||
<Suspense fallback=|| ()>
|
||||
{move || res.with(cx, |data| view.with_value(|view| view(cx, data)))}
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
@@ -147,6 +147,8 @@
|
||||
|
||||
mod additional_attributes;
|
||||
pub use additional_attributes::*;
|
||||
mod await_;
|
||||
pub use await_::*;
|
||||
pub use leptos_config::{self, get_configuration, LeptosOptions};
|
||||
#[cfg(not(all(
|
||||
target_arch = "wasm32",
|
||||
|
||||
@@ -28,75 +28,66 @@ fn main() {
|
||||
}
|
||||
|
||||
fn view_fn(cx: Scope) -> impl IntoView {
|
||||
let view = view! { cx,
|
||||
<For
|
||||
each=|| vec![0, 1, 2, 3, 4, 5, 6, 7]
|
||||
key=|i| *i
|
||||
view=|cx, i| view! { cx, {i} }
|
||||
/>
|
||||
}
|
||||
.into_view(cx);
|
||||
let (list, set_list) = create_signal(cx, vec![2]);//vec![1, 2, 3, 4, 5]);
|
||||
|
||||
let (a, set_a) = create_signal(cx, view.clone());
|
||||
let (b, set_b) = create_signal(cx, view);
|
||||
|
||||
let (is_a, set_is_a) = create_signal(cx, true);
|
||||
|
||||
let handle_toggle = move |_| {
|
||||
trace!("toggling");
|
||||
if is_a() {
|
||||
set_b(a());
|
||||
|
||||
set_is_a(false);
|
||||
} else {
|
||||
set_a(a());
|
||||
|
||||
set_is_a(true);
|
||||
}
|
||||
};
|
||||
|
||||
let a_tag = view! { cx, <svg::a/> };
|
||||
|
||||
view! { cx,
|
||||
<>
|
||||
<div>
|
||||
<button on:click=handle_toggle>"Toggle"</button>
|
||||
</div>
|
||||
<svg>{a_tag}</svg>
|
||||
<Example/>
|
||||
<A child=Signal::from(a) />
|
||||
<A child=Signal::from(b) />
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn A(cx: Scope, child: Signal<View>) -> impl IntoView {
|
||||
move || child()
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Example(cx: Scope) -> impl IntoView {
|
||||
trace!("rendering <Example/>");
|
||||
|
||||
let (value, set_value) = create_signal(cx, 10);
|
||||
|
||||
let memo = create_memo(cx, move |_| value() * 2);
|
||||
let derived = Signal::derive(cx, move || value() * 3);
|
||||
|
||||
create_effect(cx, move |_| {
|
||||
trace!("logging value of derived..., {}", derived.get());
|
||||
request_animation_frame(move || {
|
||||
set_list(vec![1, 2]);//vec![0, 1, 3, 6, 4, 5, 2, 7])
|
||||
});
|
||||
|
||||
set_timeout(
|
||||
move || set_value.update(|v| *v += 1),
|
||||
std::time::Duration::from_millis(50),
|
||||
);
|
||||
|
||||
view! { cx,
|
||||
<h1>"Example"</h1>
|
||||
<button on:click=move |_| set_value.update(|value| *value += 1)>
|
||||
"Click me"
|
||||
</button>
|
||||
<ul>
|
||||
/* These work! */
|
||||
/* <Test from=&[1] to=&[]/>
|
||||
<Test from=&[1, 2] to=&[]/>
|
||||
<Test from=&[1, 2, 3] to=&[]/>
|
||||
<Test from=&[] to=&[1]/>
|
||||
<Test from=&[1, 2] to=&[1]/>
|
||||
<Test from=&[2, 1] to=&[1]/>
|
||||
<Test from=&[1] to=&[1, 2]/>
|
||||
<Test from=&[2] to=&[1, 2]/>
|
||||
<Test from=&[1, 2, 3] to=&[1, 2]/>
|
||||
<Test from=&[] to=&[1, 2, 3]/>
|
||||
<Test from=&[2] to=&[1, 2, 3]/>
|
||||
<Test from=&[1] to=&[1, 2, 3]/>
|
||||
<Test from=&[3] to=&[1, 2, 3]/>
|
||||
|
||||
<Test from=&[1, 3, 2] to=&[1, 2, 3]/>
|
||||
<Test from=&[2, 1, 3] to=&[1, 2, 3]/>*/
|
||||
|
||||
// TODO diffing broken
|
||||
// <Test from=&[3, 2, 1] to=&[1, 2, 3]/>
|
||||
<Test from=&[3, 1] to=&[1, 2, 3]/>
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Test(cx: Scope, from: &'static [usize], to: &'static [usize]) -> impl IntoView {
|
||||
let (list, set_list) = create_signal(cx, from.to_vec());
|
||||
request_animation_frame(move || {
|
||||
set_list(to.to_vec());
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
<li>
|
||||
<For
|
||||
each=list
|
||||
key=|i| *i
|
||||
view=|cx, i| {
|
||||
view! { cx, <span>{i}</span> }
|
||||
}
|
||||
/>
|
||||
/* <p>
|
||||
"Pre | "
|
||||
<For
|
||||
each=list
|
||||
key=|i| *i
|
||||
view=|cx, i| {
|
||||
view! { cx, <span>{i}</span> }
|
||||
}
|
||||
/>
|
||||
" | Post"
|
||||
</p> */
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@@ -278,7 +278,33 @@ where
|
||||
let start = child.get_opening_node();
|
||||
let end = &closing;
|
||||
|
||||
unmount_child(&start, end);
|
||||
match child {
|
||||
View::CoreComponent(
|
||||
crate::CoreComponent::DynChild(
|
||||
child,
|
||||
),
|
||||
) => {
|
||||
let start =
|
||||
child.get_opening_node();
|
||||
let end = child.closing.node;
|
||||
prepare_to_move(
|
||||
&child.document_fragment,
|
||||
&start,
|
||||
&end,
|
||||
);
|
||||
}
|
||||
View::Component(child) => {
|
||||
let start =
|
||||
child.get_opening_node();
|
||||
let end = child.closing.node;
|
||||
prepare_to_move(
|
||||
&child.document_fragment,
|
||||
&start,
|
||||
&end,
|
||||
);
|
||||
}
|
||||
_ => unmount_child(&start, end),
|
||||
}
|
||||
}
|
||||
|
||||
// Mount the new child
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -218,7 +218,9 @@ pub fn render_to_stream_with_prefix_undisposed_with_context_and_block_replacemen
|
||||
let mut blocking_fragments = FuturesUnordered::new();
|
||||
let fragments = FuturesUnordered::new();
|
||||
|
||||
eprintln!("\n\n");
|
||||
for (fragment_id, data) in pending_fragments {
|
||||
eprintln!("pending fragment {fragment_id:?}");
|
||||
if data.should_block {
|
||||
blocking_fragments
|
||||
.push(async move { (fragment_id, data.out_of_order.await) });
|
||||
|
||||
@@ -11,7 +11,7 @@ readme = "../README.md"
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
syn = { version = "1", features = [
|
||||
syn = { version = "2", features = [
|
||||
"full",
|
||||
"parsing",
|
||||
"extra-traits",
|
||||
@@ -19,7 +19,7 @@ syn = { version = "1", features = [
|
||||
"printing",
|
||||
] }
|
||||
quote = "1"
|
||||
syn-rsx = "0.9"
|
||||
rstml = "0.10.6"
|
||||
proc-macro2 = { version = "1", features = ["span-locations", "nightly"] }
|
||||
parking_lot = "0.12"
|
||||
walkdir = "2"
|
||||
|
||||
@@ -76,7 +76,7 @@ impl ViewMacros {
|
||||
tokens.next(); // ,
|
||||
// TODO handle class = ...
|
||||
let rsx =
|
||||
syn_rsx::parse2(tokens.collect::<proc_macro2::TokenStream>())?;
|
||||
rstml::parse2(tokens.collect::<proc_macro2::TokenStream>())?;
|
||||
let template = LNode::parse_view(rsx)?;
|
||||
views.push(MacroInvocation { id, template })
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::parsing::{is_component_node, value_to_string};
|
||||
use crate::parsing::is_component_node;
|
||||
use anyhow::Result;
|
||||
use quote::quote;
|
||||
use quote::ToTokens;
|
||||
use rstml::node::{Node, NodeAttribute};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use syn_rsx::Node;
|
||||
|
||||
// A lightweight virtual DOM structure we can use to hold
|
||||
// the state of a Leptos view macro template. This is because
|
||||
@@ -58,36 +58,30 @@ impl LNode {
|
||||
}
|
||||
}
|
||||
Node::Text(text) => {
|
||||
if let Some(value) = value_to_string(&text.value) {
|
||||
views.push(LNode::Text(value));
|
||||
} else {
|
||||
let value = text.value.as_ref();
|
||||
let code = quote! { #value };
|
||||
let code = code.to_string();
|
||||
views.push(LNode::DynChild(code));
|
||||
}
|
||||
views.push(LNode::Text(text.value_string()));
|
||||
}
|
||||
Node::Block(block) => {
|
||||
let value = block.value.as_ref();
|
||||
let code = quote! { #value };
|
||||
let code = block.into_token_stream();
|
||||
let code = code.to_string();
|
||||
views.push(LNode::DynChild(code));
|
||||
}
|
||||
Node::Element(el) => {
|
||||
if is_component_node(&el) {
|
||||
let name = el.name().to_string();
|
||||
let mut children = Vec::new();
|
||||
for child in el.children {
|
||||
LNode::parse_node(child, &mut children)?;
|
||||
}
|
||||
views.push(LNode::Component {
|
||||
name: el.name.to_string(),
|
||||
name: name,
|
||||
props: el
|
||||
.open_tag
|
||||
.attributes
|
||||
.into_iter()
|
||||
.filter_map(|attr| match attr {
|
||||
Node::Attribute(attr) => Some((
|
||||
NodeAttribute::Attribute(attr) => Some((
|
||||
attr.key.to_string(),
|
||||
format!("{:#?}", attr.value),
|
||||
format!("{:#?}", attr.value()),
|
||||
)),
|
||||
_ => None,
|
||||
})
|
||||
@@ -95,15 +89,13 @@ impl LNode {
|
||||
children,
|
||||
});
|
||||
} else {
|
||||
let name = el.name.to_string();
|
||||
let name = el.name().to_string();
|
||||
let mut attrs = Vec::new();
|
||||
|
||||
for attr in el.attributes {
|
||||
if let Node::Attribute(attr) = attr {
|
||||
for attr in el.open_tag.attributes {
|
||||
if let NodeAttribute::Attribute(attr) = attr {
|
||||
let name = attr.key.to_string();
|
||||
if let Some(value) =
|
||||
attr.value.as_ref().and_then(value_to_string)
|
||||
{
|
||||
if let Some(value) = attr.value_literal_string() {
|
||||
attrs.push((
|
||||
name,
|
||||
LAttributeValue::Static(value),
|
||||
|
||||
@@ -1,7 +1,37 @@
|
||||
use syn_rsx::{NodeElement, NodeValueExpr};
|
||||
use rstml::node::NodeElement;
|
||||
|
||||
pub fn value_to_string(value: &NodeValueExpr) -> Option<String> {
|
||||
match &value.as_ref() {
|
||||
///
|
||||
/// Converts `syn::Block` to simple expression
|
||||
///
|
||||
/// For example:
|
||||
/// ```no_build
|
||||
/// // "string literal" in
|
||||
/// {"string literal"}
|
||||
/// // number literal
|
||||
/// {0x12}
|
||||
/// // boolean literal
|
||||
/// {true}
|
||||
/// // variable
|
||||
/// {path::x}
|
||||
/// ```
|
||||
pub fn block_to_primitive_expression(block: &syn::Block) -> Option<&syn::Expr> {
|
||||
// its empty block, or block with multi lines
|
||||
if block.stmts.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
match &block.stmts[0] {
|
||||
syn::Stmt::Expr(e, None) => return Some(&e),
|
||||
_ => {}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Converts simple literals to its string representation.
|
||||
///
|
||||
/// This function doesn't convert literal wrapped inside block
|
||||
/// like: `{"string"}`.
|
||||
pub fn value_to_string(value: &syn::Expr) -> Option<String> {
|
||||
match &value {
|
||||
syn::Expr::Lit(lit) => match &lit.lit {
|
||||
syn::Lit::Str(s) => Some(s.value()),
|
||||
syn::Lit::Char(c) => Some(c.value().to_string()),
|
||||
@@ -14,7 +44,7 @@ pub fn value_to_string(value: &NodeValueExpr) -> Option<String> {
|
||||
}
|
||||
|
||||
pub fn is_component_node(node: &NodeElement) -> bool {
|
||||
node.name
|
||||
node.name()
|
||||
.to_string()
|
||||
.starts_with(|c: char| c.is_ascii_uppercase())
|
||||
}
|
||||
|
||||
@@ -12,16 +12,16 @@ readme = "../README.md"
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
attribute-derive = { version = "0.5", features = ["syn-full"] }
|
||||
attribute-derive = { version = "0.6", features = ["syn-full"] }
|
||||
cfg-if = "1"
|
||||
html-escape = "0.2"
|
||||
itertools = "0.10"
|
||||
prettyplease = "0.1"
|
||||
prettyplease = "0.2.4"
|
||||
proc-macro-error = "1"
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
syn = { version = "1", features = ["full"] }
|
||||
syn-rsx = "0.9"
|
||||
syn = { version = "2", features = ["full"] }
|
||||
rstml = "0.10.6"
|
||||
leptos_hot_reload = { workspace = true }
|
||||
server_fn_macro = { workspace = true }
|
||||
convert_case = "0.6.0"
|
||||
|
||||
@@ -4,15 +4,15 @@ use convert_case::{
|
||||
Casing,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use leptos_hot_reload::parsing::value_to_string;
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{format_ident, quote_spanned, ToTokens, TokenStreamExt};
|
||||
use syn::{
|
||||
parse::Parse, parse_quote, spanned::Spanned,
|
||||
AngleBracketedGenericArguments, Attribute, FnArg, GenericArgument, Item,
|
||||
ItemFn, Lit, LitStr, Meta, MetaNameValue, Pat, PatIdent, Path,
|
||||
PathArguments, ReturnType, Stmt, Type, TypePath, Visibility,
|
||||
ItemFn, LitStr, Meta, Pat, PatIdent, Path, PathArguments, ReturnType, Stmt,
|
||||
Type, TypePath, Visibility,
|
||||
};
|
||||
|
||||
pub struct Model {
|
||||
is_transparent: bool,
|
||||
docs: Docs,
|
||||
@@ -56,14 +56,17 @@ impl Parse for Model {
|
||||
|
||||
// We need to remove the `#[doc = ""]` and `#[builder(_)]`
|
||||
// attrs from the function signature
|
||||
drain_filter(&mut item.attrs, |attr| {
|
||||
attr.path == parse_quote!(doc) || attr.path == parse_quote!(prop)
|
||||
drain_filter(&mut item.attrs, |attr| match &attr.meta {
|
||||
Meta::NameValue(attr) => attr.path == parse_quote!(doc),
|
||||
Meta::List(attr) => attr.path == parse_quote!(prop),
|
||||
_ => false,
|
||||
});
|
||||
item.sig.inputs.iter_mut().for_each(|arg| {
|
||||
if let FnArg::Typed(ty) = arg {
|
||||
drain_filter(&mut ty.attrs, |attr| {
|
||||
attr.path == parse_quote!(doc)
|
||||
|| attr.path == parse_quote!(prop)
|
||||
drain_filter(&mut ty.attrs, |attr| match &attr.meta {
|
||||
Meta::NameValue(attr) => attr.path == parse_quote!(doc),
|
||||
Meta::List(attr) => attr.path == parse_quote!(prop),
|
||||
_ => false,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -400,12 +403,20 @@ impl Docs {
|
||||
|
||||
let mut attrs = attrs
|
||||
.iter()
|
||||
.filter_map(|attr| attr.path.is_ident("doc").then(|| {
|
||||
let Ok(Meta::NameValue(MetaNameValue { lit: Lit::Str(doc), .. })) = attr.parse_meta() else {
|
||||
abort!(attr, "expected doc comment to be string literal");
|
||||
.filter_map(|attr| {
|
||||
let Meta::NameValue(attr ) = &attr.meta else {
|
||||
return None
|
||||
};
|
||||
(doc.value(), doc.span())
|
||||
}))
|
||||
if !attr.path.is_ident("doc") {
|
||||
return None
|
||||
}
|
||||
|
||||
let Some(val) = value_to_string(&attr.value) else {
|
||||
abort!(attr, "expected string literal in value of doc comment");
|
||||
};
|
||||
|
||||
Some((val, attr.path.span()))
|
||||
})
|
||||
.flat_map(map)
|
||||
.collect_vec();
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ extern crate proc_macro_error;
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::{Span, TokenTree};
|
||||
use quote::ToTokens;
|
||||
use rstml::{node::KeyedAttribute, parse};
|
||||
use server_fn_macro::{server_macro_impl, ServerContext};
|
||||
use syn::parse_macro_input;
|
||||
use syn_rsx::{parse, NodeAttribute};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Mode {
|
||||
@@ -351,16 +351,22 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
.chain(tokens)
|
||||
.collect()
|
||||
};
|
||||
|
||||
match parse(tokens.into()) {
|
||||
Ok(nodes) => render_view(
|
||||
&proc_macro2::Ident::new(&cx.to_string(), cx.span()),
|
||||
&nodes,
|
||||
Mode::default(),
|
||||
global_class.as_ref(),
|
||||
normalized_call_site(proc_macro::Span::call_site()),
|
||||
),
|
||||
Err(error) => error.to_compile_error(),
|
||||
let config = rstml::ParserConfig::default().recover_block(true);
|
||||
let parser = rstml::Parser::new(config);
|
||||
let (nodes, errors) = parser.parse_recoverable(tokens).split_vec();
|
||||
let errors = errors.into_iter().map(|e| e.emit_as_expr_tokens());
|
||||
let nodes_output = render_view(
|
||||
&cx,
|
||||
&nodes,
|
||||
Mode::default(),
|
||||
global_class.as_ref(),
|
||||
normalized_call_site(proc_macro::Span::call_site()),
|
||||
);
|
||||
quote! {
|
||||
{
|
||||
#(#errors;)*
|
||||
#nodes_output
|
||||
}
|
||||
}
|
||||
.into()
|
||||
}
|
||||
@@ -874,9 +880,9 @@ pub fn params_derive(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn attribute_value(attr: &NodeAttribute) -> &syn::Expr {
|
||||
match &attr.value {
|
||||
Some(value) => value.as_ref(),
|
||||
pub(crate) fn attribute_value(attr: &KeyedAttribute) -> &syn::Expr {
|
||||
match &attr.possible_value {
|
||||
Some(value) => &value.value,
|
||||
None => abort!(attr.key, "attribute should have value"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ use attribute_derive::Attribute as AttributeDerive;
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
use quote::{ToTokens, TokenStreamExt};
|
||||
use syn::{
|
||||
parse::Parse, parse_quote, Field, ItemStruct, LitStr, Type, Visibility,
|
||||
parse::Parse, parse_quote, Field, ItemStruct, LitStr, Meta, Type,
|
||||
Visibility,
|
||||
};
|
||||
|
||||
pub struct Model {
|
||||
@@ -31,13 +32,16 @@ impl Parse for Model {
|
||||
|
||||
// We need to remove the `#[doc = ""]` and `#[builder(_)]`
|
||||
// attrs from the function signature
|
||||
drain_filter(&mut item.attrs, |attr| {
|
||||
attr.path == parse_quote!(doc) || attr.path == parse_quote!(prop)
|
||||
drain_filter(&mut item.attrs, |attr| match &attr.meta {
|
||||
Meta::NameValue(attr) => attr.path == parse_quote!(doc),
|
||||
Meta::List(attr) => attr.path == parse_quote!(prop),
|
||||
_ => false,
|
||||
});
|
||||
item.fields.iter_mut().for_each(|arg| {
|
||||
drain_filter(&mut arg.attrs, |attr| {
|
||||
attr.path == parse_quote!(doc)
|
||||
|| attr.path == parse_quote!(prop)
|
||||
drain_filter(&mut arg.attrs, |attr| match &attr.meta {
|
||||
Meta::NameValue(attr) => attr.path == parse_quote!(doc),
|
||||
Meta::List(attr) => attr.path == parse_quote!(prop),
|
||||
_ => false,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
use crate::attribute_value;
|
||||
use leptos_hot_reload::parsing::is_component_node;
|
||||
use itertools::Either;
|
||||
use leptos_hot_reload::parsing::{
|
||||
block_to_primitive_expression, is_component_node, value_to_string,
|
||||
};
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{quote, quote_spanned};
|
||||
use quote::{quote, quote_spanned, ToTokens};
|
||||
use rstml::node::{
|
||||
KeyedAttribute, Node, NodeAttribute, NodeBlock, NodeElement,
|
||||
};
|
||||
use syn::spanned::Spanned;
|
||||
use syn_rsx::{Node, NodeAttribute, NodeElement, NodeValueExpr};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(crate) fn render_template(cx: &Ident, nodes: &[Node]) -> TokenStream {
|
||||
@@ -53,7 +58,7 @@ fn root_element_to_tokens(
|
||||
.unwrap();
|
||||
};
|
||||
|
||||
let span = node.name.span();
|
||||
let span = node.name().span();
|
||||
|
||||
let navigations = if navigations.is_empty() {
|
||||
quote! {}
|
||||
@@ -67,7 +72,7 @@ fn root_element_to_tokens(
|
||||
quote! { #(#expressions;);* }
|
||||
};
|
||||
|
||||
let tag_name = node.name.to_string();
|
||||
let tag_name = node.name().to_string();
|
||||
|
||||
quote_spanned! {
|
||||
span => {
|
||||
@@ -104,9 +109,9 @@ enum PrevSibChange {
|
||||
Skip,
|
||||
}
|
||||
|
||||
fn attributes(node: &NodeElement) -> impl Iterator<Item = &NodeAttribute> {
|
||||
node.attributes.iter().filter_map(|node| {
|
||||
if let Node::Attribute(attribute) = node {
|
||||
fn attributes(node: &NodeElement) -> impl Iterator<Item = &KeyedAttribute> {
|
||||
node.attributes().iter().filter_map(|node| {
|
||||
if let NodeAttribute::Attribute(attribute) = node {
|
||||
Some(attribute)
|
||||
} else {
|
||||
None
|
||||
@@ -129,11 +134,11 @@ fn element_to_tokens(
|
||||
) -> Ident {
|
||||
// create this element
|
||||
*next_el_id += 1;
|
||||
let this_el_ident = child_ident(*next_el_id, node.name.span());
|
||||
let this_el_ident = child_ident(*next_el_id, node.name().span());
|
||||
|
||||
// Open tag
|
||||
let name_str = node.name.to_string();
|
||||
let span = node.name.span();
|
||||
let name_str = node.name().to_string();
|
||||
let span = node.name().span();
|
||||
|
||||
// CSR/hydrate, push to template
|
||||
template.push('<');
|
||||
@@ -145,7 +150,7 @@ fn element_to_tokens(
|
||||
}
|
||||
|
||||
// navigation for this el
|
||||
let debug_name = node.name.to_string();
|
||||
let debug_name = node.name().to_string();
|
||||
let this_nav = if is_root_el {
|
||||
quote_spanned! {
|
||||
span => let #this_el_ident = #debug_name;
|
||||
@@ -247,14 +252,17 @@ fn next_sibling_node(
|
||||
if is_component_node(sibling) {
|
||||
next_sibling_node(children, idx + 1, next_el_id)
|
||||
} else {
|
||||
Ok(Some(child_ident(*next_el_id + 1, sibling.name.span())))
|
||||
Ok(Some(child_ident(
|
||||
*next_el_id + 1,
|
||||
sibling.name().span(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
Node::Block(sibling) => {
|
||||
Ok(Some(child_ident(*next_el_id + 1, sibling.value.span())))
|
||||
Ok(Some(child_ident(*next_el_id + 1, sibling.span())))
|
||||
}
|
||||
Node::Text(sibling) => {
|
||||
Ok(Some(child_ident(*next_el_id + 1, sibling.value.span())))
|
||||
Ok(Some(child_ident(*next_el_id + 1, sibling.span())))
|
||||
}
|
||||
_ => Err("expected either an element or a block".to_string()),
|
||||
}
|
||||
@@ -263,7 +271,7 @@ fn next_sibling_node(
|
||||
|
||||
fn attr_to_tokens(
|
||||
cx: &Ident,
|
||||
node: &NodeAttribute,
|
||||
node: &KeyedAttribute,
|
||||
el_id: &Ident,
|
||||
template: &mut String,
|
||||
expressions: &mut Vec<TokenStream>,
|
||||
@@ -272,8 +280,8 @@ fn attr_to_tokens(
|
||||
let name = name.strip_prefix('_').unwrap_or(&name);
|
||||
let name = name.strip_prefix("attr:").unwrap_or(name);
|
||||
|
||||
let value = match &node.value {
|
||||
Some(expr) => match expr.as_ref() {
|
||||
let value = match &node.value() {
|
||||
Some(expr) => match expr {
|
||||
syn::Expr::Lit(expr_lit) => {
|
||||
if let syn::Lit::Str(s) = &expr_lit.lit {
|
||||
AttributeValue::Static(s.value())
|
||||
@@ -367,7 +375,7 @@ fn child_to_tokens(
|
||||
Node::Element(node) => {
|
||||
if is_component_node(node) {
|
||||
proc_macro_error::emit_error!(
|
||||
node.name.span(),
|
||||
node.name().span(),
|
||||
"component children not allowed in template!, use view! \
|
||||
instead"
|
||||
);
|
||||
@@ -389,7 +397,7 @@ fn child_to_tokens(
|
||||
}
|
||||
Node::Text(node) => block_to_tokens(
|
||||
cx,
|
||||
&node.value,
|
||||
Either::Left(node.value_string()),
|
||||
node.value.span(),
|
||||
parent,
|
||||
prev_sib,
|
||||
@@ -399,10 +407,42 @@ fn child_to_tokens(
|
||||
expressions,
|
||||
navigations,
|
||||
),
|
||||
Node::Block(node) => block_to_tokens(
|
||||
Node::RawText(node) => block_to_tokens(
|
||||
cx,
|
||||
&node.value,
|
||||
node.value.span(),
|
||||
Either::Left(node.to_string_best()),
|
||||
node.span(),
|
||||
parent,
|
||||
prev_sib,
|
||||
next_sib,
|
||||
next_el_id,
|
||||
template,
|
||||
expressions,
|
||||
navigations,
|
||||
),
|
||||
Node::Block(NodeBlock::ValidBlock(b)) => {
|
||||
let value = match block_to_primitive_expression(b)
|
||||
.and_then(value_to_string)
|
||||
{
|
||||
Some(v) => Either::Left(v),
|
||||
None => Either::Right(b.into_token_stream()),
|
||||
};
|
||||
block_to_tokens(
|
||||
cx,
|
||||
value,
|
||||
b.span(),
|
||||
parent,
|
||||
prev_sib,
|
||||
next_sib,
|
||||
next_el_id,
|
||||
template,
|
||||
expressions,
|
||||
navigations,
|
||||
)
|
||||
}
|
||||
Node::Block(b @ NodeBlock::Invalid { .. }) => block_to_tokens(
|
||||
cx,
|
||||
Either::Right(b.into_token_stream()),
|
||||
b.span(),
|
||||
parent,
|
||||
prev_sib,
|
||||
next_sib,
|
||||
@@ -418,7 +458,7 @@ fn child_to_tokens(
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn block_to_tokens(
|
||||
_cx: &Ident,
|
||||
value: &NodeValueExpr,
|
||||
value: Either<String, TokenStream>,
|
||||
span: Span,
|
||||
parent: &Ident,
|
||||
prev_sib: Option<Ident>,
|
||||
@@ -428,18 +468,6 @@ fn block_to_tokens(
|
||||
expressions: &mut Vec<TokenStream>,
|
||||
navigations: &mut Vec<TokenStream>,
|
||||
) -> PrevSibChange {
|
||||
let value = value.as_ref();
|
||||
let str_value = match value {
|
||||
syn::Expr::Lit(lit) => match &lit.lit {
|
||||
syn::Lit::Str(s) => Some(s.value()),
|
||||
syn::Lit::Char(c) => Some(c.value().to_string()),
|
||||
syn::Lit::Int(i) => Some(i.base10_digits().to_string()),
|
||||
syn::Lit::Float(f) => Some(f.base10_digits().to_string()),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// code to navigate to this text node
|
||||
|
||||
let (name, location) = /* if is_first_child && mode == Mode::Client {
|
||||
@@ -473,27 +501,30 @@ fn block_to_tokens(
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(v) = str_value {
|
||||
navigations.push(location);
|
||||
template.push_str(&v);
|
||||
match value {
|
||||
Either::Left(v) => {
|
||||
navigations.push(location);
|
||||
template.push_str(&v);
|
||||
|
||||
if let Some(name) = name {
|
||||
PrevSibChange::Sib(name)
|
||||
} else {
|
||||
PrevSibChange::Parent
|
||||
if let Some(name) = name {
|
||||
PrevSibChange::Sib(name)
|
||||
} else {
|
||||
PrevSibChange::Parent
|
||||
}
|
||||
}
|
||||
} else {
|
||||
template.push_str("<!>");
|
||||
navigations.push(location);
|
||||
Either::Right(value) => {
|
||||
template.push_str("<!>");
|
||||
navigations.push(location);
|
||||
|
||||
expressions.push(quote! {
|
||||
leptos::leptos_dom::mount_child(#mount_kind, &{#value}.into_view(cx));
|
||||
});
|
||||
expressions.push(quote! {
|
||||
leptos::leptos_dom::mount_child(#mount_kind, &{#value}.into_view(cx));
|
||||
});
|
||||
|
||||
if let Some(name) = name {
|
||||
PrevSibChange::Sib(name)
|
||||
} else {
|
||||
PrevSibChange::Parent
|
||||
if let Some(name) = name {
|
||||
PrevSibChange::Sib(name)
|
||||
} else {
|
||||
PrevSibChange::Parent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
use crate::{attribute_value, Mode};
|
||||
use convert_case::{Case::Snake, Casing};
|
||||
use leptos_hot_reload::parsing::{is_component_node, value_to_string};
|
||||
use leptos_hot_reload::parsing::{
|
||||
block_to_primitive_expression, is_component_node, value_to_string,
|
||||
};
|
||||
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
|
||||
use quote::{format_ident, quote, quote_spanned};
|
||||
use rstml::node::{
|
||||
KeyedAttribute, Node, NodeAttribute, NodeBlock, NodeElement, NodeName,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use syn::{spanned::Spanned, Expr, ExprLit, ExprPath, Lit};
|
||||
use syn_rsx::{Node, NodeAttribute, NodeElement, NodeName, NodeValueExpr};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum TagType {
|
||||
@@ -213,18 +217,22 @@ fn root_node_to_tokens_ssr(
|
||||
global_class,
|
||||
view_marker,
|
||||
),
|
||||
Node::Comment(_) | Node::Doctype(_) | Node::Attribute(_) => quote! {},
|
||||
Node::Comment(_) | Node::Doctype(_) => quote! {},
|
||||
Node::Text(node) => {
|
||||
let value = node.value.as_ref();
|
||||
quote! {
|
||||
leptos::leptos_dom::html::text(#value)
|
||||
leptos::leptos_dom::html::text(#node)
|
||||
}
|
||||
}
|
||||
Node::RawText(r) => {
|
||||
let text = r.to_string_best();
|
||||
let text = syn::LitStr::new(&text, r.span());
|
||||
quote! {
|
||||
leptos::leptos_dom::html::text(#text)
|
||||
}
|
||||
}
|
||||
Node::Block(node) => {
|
||||
let value = node.value.as_ref();
|
||||
quote! {
|
||||
#[allow(unused_braces)]
|
||||
#value
|
||||
#node
|
||||
}
|
||||
}
|
||||
Node::Element(node) => {
|
||||
@@ -254,9 +262,9 @@ fn fragment_to_tokens_ssr(
|
||||
});
|
||||
quote! {
|
||||
{
|
||||
leptos::Fragment::lazy(|| vec![
|
||||
leptos::Fragment::lazy(|| [
|
||||
#(#nodes),*
|
||||
])
|
||||
].to_vec())
|
||||
#view_marker
|
||||
}
|
||||
}
|
||||
@@ -329,15 +337,15 @@ fn root_element_to_tokens_ssr(
|
||||
},
|
||||
});
|
||||
|
||||
let tag_name = node.name.to_string();
|
||||
let tag_name = node.name().to_string();
|
||||
let is_custom_element = is_custom_element(&tag_name);
|
||||
let typed_element_name = if is_custom_element {
|
||||
Ident::new("Custom", node.name.span())
|
||||
Ident::new("Custom", node.name().span())
|
||||
} else {
|
||||
let camel_cased = camel_case_tag_name(
|
||||
&tag_name.replace("svg::", "").replace("math::", ""),
|
||||
);
|
||||
Ident::new(&camel_cased, node.name.span())
|
||||
Ident::new(&camel_cased, node.name().span())
|
||||
};
|
||||
let typed_element_name = if is_svg_element(&tag_name) {
|
||||
quote! { svg::#typed_element_name }
|
||||
@@ -409,7 +417,7 @@ fn element_to_tokens_ssr(
|
||||
}));
|
||||
} else {
|
||||
let tag_name = node
|
||||
.name
|
||||
.name()
|
||||
.to_string()
|
||||
.replace("svg::", "")
|
||||
.replace("math::", "");
|
||||
@@ -419,8 +427,8 @@ fn element_to_tokens_ssr(
|
||||
|
||||
let mut inner_html = None;
|
||||
|
||||
for attr in &node.attributes {
|
||||
if let Node::Attribute(attr) = attr {
|
||||
for attr in node.attributes() {
|
||||
if let NodeAttribute::Attribute(attr) = attr {
|
||||
inner_html = attribute_to_tokens_ssr(
|
||||
cx,
|
||||
attr,
|
||||
@@ -439,9 +447,9 @@ fn element_to_tokens_ssr(
|
||||
quote! { leptos::leptos_dom::HydrationCtx::id() }
|
||||
};
|
||||
match node
|
||||
.attributes
|
||||
.attributes()
|
||||
.iter()
|
||||
.find(|node| matches!(node, Node::Attribute(attr) if attr.key.to_string() == "id"))
|
||||
.find(|node| matches!(node, NodeAttribute::Attribute(attr) if attr.key.to_string() == "id"))
|
||||
{
|
||||
Some(_) => {
|
||||
template.push_str(" leptos-hk=\"_{}\"");
|
||||
@@ -462,7 +470,7 @@ fn element_to_tokens_ssr(
|
||||
|
||||
if let Some(inner_html) = inner_html {
|
||||
template.push_str("{}");
|
||||
let value = inner_html.as_ref();
|
||||
let value = inner_html;
|
||||
|
||||
holes.push(quote! {
|
||||
(#value).into_attribute(#cx).as_nameless_value_string().unwrap_or_default()
|
||||
@@ -484,32 +492,23 @@ fn element_to_tokens_ssr(
|
||||
);
|
||||
}
|
||||
Node::Text(text) => {
|
||||
if let Some(value) = value_to_string(&text.value) {
|
||||
let value = if is_script_or_style {
|
||||
value.into()
|
||||
} else {
|
||||
html_escape::encode_safe(&value)
|
||||
};
|
||||
template.push_str(
|
||||
&value
|
||||
.replace('{', "\\{")
|
||||
.replace('}', "\\}"),
|
||||
);
|
||||
let value = text.value_string();
|
||||
let value = if is_script_or_style {
|
||||
value.into()
|
||||
} else {
|
||||
template.push_str("{}");
|
||||
let value = text.value.as_ref();
|
||||
|
||||
holes.push(quote! {
|
||||
#value.into_view(#cx).render_to_string(#cx)
|
||||
})
|
||||
}
|
||||
html_escape::encode_safe(&value)
|
||||
};
|
||||
template.push_str(
|
||||
&value.replace('{', "\\{").replace('}', "\\}"),
|
||||
);
|
||||
}
|
||||
Node::Block(block) => {
|
||||
if let Some(value) = value_to_string(&block.value) {
|
||||
Node::Block(NodeBlock::ValidBlock(block)) => {
|
||||
if let Some(value) =
|
||||
block_to_primitive_expression(block)
|
||||
.and_then(value_to_string)
|
||||
{
|
||||
template.push_str(&value);
|
||||
} else {
|
||||
let value = block.value.as_ref();
|
||||
|
||||
if !template.is_empty() {
|
||||
chunks.push(SsrElementChunks::String {
|
||||
template: std::mem::take(template),
|
||||
@@ -517,10 +516,16 @@ fn element_to_tokens_ssr(
|
||||
})
|
||||
}
|
||||
chunks.push(SsrElementChunks::View(quote! {
|
||||
{#value}.into_view(#cx)
|
||||
{#block}.into_view(#cx)
|
||||
}));
|
||||
}
|
||||
}
|
||||
// Keep invalid blocks for faster IDE diff (on user type)
|
||||
Node::Block(block @ NodeBlock::Invalid { .. }) => {
|
||||
chunks.push(SsrElementChunks::View(quote! {
|
||||
{#block}.into_view(#cx)
|
||||
}));
|
||||
}
|
||||
Node::Fragment(_) => abort!(
|
||||
Span::call_site(),
|
||||
"You can't nest a fragment inside an element."
|
||||
@@ -531,7 +536,7 @@ fn element_to_tokens_ssr(
|
||||
}
|
||||
|
||||
template.push_str("</");
|
||||
template.push_str(&node.name.to_string());
|
||||
template.push_str(&node.name().to_string());
|
||||
template.push('>');
|
||||
}
|
||||
}
|
||||
@@ -540,17 +545,17 @@ fn element_to_tokens_ssr(
|
||||
// returns `inner_html`
|
||||
fn attribute_to_tokens_ssr<'a>(
|
||||
cx: &Ident,
|
||||
node: &'a NodeAttribute,
|
||||
attr: &'a KeyedAttribute,
|
||||
template: &mut String,
|
||||
holes: &mut Vec<TokenStream>,
|
||||
exprs_for_compiler: &mut Vec<TokenStream>,
|
||||
global_class: Option<&TokenTree>,
|
||||
) -> Option<&'a NodeValueExpr> {
|
||||
let name = node.key.to_string();
|
||||
) -> Option<&'a syn::Expr> {
|
||||
let name = attr.key.to_string();
|
||||
if name == "ref" || name == "_ref" || name == "ref_" || name == "node_ref" {
|
||||
// ignore refs on SSR
|
||||
} else if let Some(name) = name.strip_prefix("on:") {
|
||||
let handler = attribute_value(node);
|
||||
let handler = attribute_value(attr);
|
||||
let (event_type, _, _) = parse_event_name(name);
|
||||
|
||||
exprs_for_compiler.push(quote! {
|
||||
@@ -563,16 +568,16 @@ fn attribute_to_tokens_ssr<'a>(
|
||||
// ignore props for SSR
|
||||
// ignore classes and sdtyles: we'll handle these separately
|
||||
} else if name == "inner_html" {
|
||||
return node.value.as_ref();
|
||||
return attr.value();
|
||||
} else {
|
||||
let name = name.replacen("attr:", "", 1);
|
||||
|
||||
// special case of global_class and class attribute
|
||||
if name == "class"
|
||||
&& global_class.is_some()
|
||||
&& node.value.as_ref().and_then(value_to_string).is_none()
|
||||
&& attr.value().and_then(value_to_string).is_none()
|
||||
{
|
||||
let span = node.key.span();
|
||||
let span = attr.key.span();
|
||||
proc_macro_error::emit_error!(span, "Combining a global class (view! { cx, class = ... }) \
|
||||
and a dynamic `class=` attribute on an element causes runtime inconsistencies. You can \
|
||||
toggle individual classes dynamically with the `class:name=value` syntax. \n\nSee this issue \
|
||||
@@ -582,7 +587,7 @@ fn attribute_to_tokens_ssr<'a>(
|
||||
if name != "class" && name != "style" {
|
||||
template.push(' ');
|
||||
|
||||
if let Some(value) = node.value.as_ref() {
|
||||
if let Some(value) = attr.value() {
|
||||
if let Some(value) = value_to_string(value) {
|
||||
template.push_str(&name);
|
||||
template.push_str("=\"");
|
||||
@@ -590,7 +595,6 @@ fn attribute_to_tokens_ssr<'a>(
|
||||
template.push('"');
|
||||
} else {
|
||||
template.push_str("{}");
|
||||
let value = value.as_ref();
|
||||
holes.push(quote! {
|
||||
&{#value}.into_attribute(#cx)
|
||||
.as_nameless_value_string()
|
||||
@@ -630,11 +634,13 @@ fn set_class_attribute_ssr(
|
||||
Some(val) => (String::new(), Some(val)),
|
||||
};
|
||||
let static_class_attr = node
|
||||
.attributes
|
||||
.attributes()
|
||||
.iter()
|
||||
.filter_map(|a| match a {
|
||||
Node::Attribute(attr) if attr.key.to_string() == "class" => {
|
||||
attr.value.as_ref().and_then(value_to_string)
|
||||
NodeAttribute::Attribute(attr)
|
||||
if attr.key.to_string() == "class" =>
|
||||
{
|
||||
attr.value().and_then(value_to_string)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
@@ -644,17 +650,17 @@ fn set_class_attribute_ssr(
|
||||
.join(" ");
|
||||
|
||||
let dyn_class_attr = node
|
||||
.attributes
|
||||
.attributes()
|
||||
.iter()
|
||||
.filter_map(|a| {
|
||||
if let Node::Attribute(a) = a {
|
||||
if let NodeAttribute::Attribute(a) = a {
|
||||
if a.key.to_string() == "class" {
|
||||
if a.value.as_ref().and_then(value_to_string).is_some()
|
||||
if a.value().and_then(value_to_string).is_some()
|
||||
|| fancy_class_name(&a.key.to_string(), cx, a).is_some()
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some((a.key.span(), &a.value))
|
||||
Some((a.key.span(), a.value()))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
@@ -666,10 +672,10 @@ fn set_class_attribute_ssr(
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let class_attrs = node
|
||||
.attributes
|
||||
.attributes()
|
||||
.iter()
|
||||
.filter_map(|node| {
|
||||
if let Node::Attribute(node) = node {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
let name = node.key.to_string();
|
||||
if name == "class" {
|
||||
return if let Some((_, name, value)) =
|
||||
@@ -713,7 +719,6 @@ fn set_class_attribute_ssr(
|
||||
for (_span, value) in dyn_class_attr {
|
||||
if let Some(value) = value {
|
||||
template.push_str(" {}");
|
||||
let value = value.as_ref();
|
||||
holes.push(quote! {
|
||||
&(#cx, #value).into_attribute(#cx).as_nameless_value_string()
|
||||
.map(|a| leptos::leptos_dom::ssr::escape_attr(&a).to_string())
|
||||
@@ -745,11 +750,13 @@ fn set_style_attribute_ssr(
|
||||
holes: &mut Vec<TokenStream>,
|
||||
) {
|
||||
let static_style_attr = node
|
||||
.attributes
|
||||
.attributes()
|
||||
.iter()
|
||||
.filter_map(|a| match a {
|
||||
Node::Attribute(attr) if attr.key.to_string() == "style" => {
|
||||
attr.value.as_ref().and_then(value_to_string)
|
||||
NodeAttribute::Attribute(attr)
|
||||
if attr.key.to_string() == "style" =>
|
||||
{
|
||||
attr.value().and_then(value_to_string)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
@@ -757,17 +764,17 @@ fn set_style_attribute_ssr(
|
||||
.map(|style| format!("{style};"));
|
||||
|
||||
let dyn_style_attr = node
|
||||
.attributes
|
||||
.attributes()
|
||||
.iter()
|
||||
.filter_map(|a| {
|
||||
if let Node::Attribute(a) = a {
|
||||
if let NodeAttribute::Attribute(a) = a {
|
||||
if a.key.to_string() == "style" {
|
||||
if a.value.as_ref().and_then(value_to_string).is_some()
|
||||
if a.value().and_then(value_to_string).is_some()
|
||||
|| fancy_style_name(&a.key.to_string(), cx, a).is_some()
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some((a.key.span(), &a.value))
|
||||
Some((a.key.span(), a.value()))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
@@ -779,10 +786,10 @@ fn set_style_attribute_ssr(
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let style_attrs = node
|
||||
.attributes
|
||||
.attributes()
|
||||
.iter()
|
||||
.filter_map(|node| {
|
||||
if let Node::Attribute(node) = node {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
let name = node.key.to_string();
|
||||
if name == "style" {
|
||||
return if let Some((_, name, value)) =
|
||||
@@ -825,7 +832,6 @@ fn set_style_attribute_ssr(
|
||||
for (_span, value) in dyn_style_attr {
|
||||
if let Some(value) = value {
|
||||
template.push_str(" {};");
|
||||
let value = value.as_ref();
|
||||
holes.push(quote! {
|
||||
&(#cx, #value).into_attribute(#cx).as_nameless_value_string()
|
||||
.map(|a| leptos::leptos_dom::ssr::escape_attr(&a).to_string())
|
||||
@@ -899,18 +905,18 @@ fn fragment_to_tokens(
|
||||
let tokens = if lazy {
|
||||
quote! {
|
||||
{
|
||||
leptos::Fragment::lazy(|| vec![
|
||||
leptos::Fragment::lazy(|| [
|
||||
#(#nodes),*
|
||||
])
|
||||
].to_vec())
|
||||
#view_marker
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
{
|
||||
leptos::Fragment::new(vec![
|
||||
leptos::Fragment::new([
|
||||
#(#nodes),*
|
||||
])
|
||||
].to_vec())
|
||||
#view_marker
|
||||
}
|
||||
}
|
||||
@@ -948,18 +954,14 @@ fn node_to_tokens(
|
||||
view_marker,
|
||||
),
|
||||
Node::Comment(_) | Node::Doctype(_) => Some(quote! {}),
|
||||
Node::Text(node) => {
|
||||
let value = node.value.as_ref();
|
||||
Some(quote! {
|
||||
leptos::leptos_dom::html::text(#value)
|
||||
})
|
||||
}
|
||||
Node::Block(node) => {
|
||||
let value = node.value.as_ref();
|
||||
Some(quote! { #value })
|
||||
}
|
||||
Node::Attribute(node) => {
|
||||
Some(attribute_to_tokens(cx, node, global_class))
|
||||
Node::Text(node) => Some(quote! {
|
||||
leptos::leptos_dom::html::text(#node)
|
||||
}),
|
||||
Node::Block(node) => Some(quote! { #node }),
|
||||
Node::RawText(r) => {
|
||||
let text = r.to_string_best();
|
||||
let text = syn::LitStr::new(&text, r.span());
|
||||
Some(quote! { #text })
|
||||
}
|
||||
Node::Element(node) => element_to_tokens(
|
||||
cx,
|
||||
@@ -980,6 +982,7 @@ fn element_to_tokens(
|
||||
global_class: Option<&TokenTree>,
|
||||
view_marker: Option<String>,
|
||||
) -> Option<TokenStream> {
|
||||
let name = node.name();
|
||||
if is_component_node(node) {
|
||||
if let Some(slot) = get_slot(node) {
|
||||
slot_to_tokens(cx, node, slot, parent_slots, global_class);
|
||||
@@ -988,20 +991,17 @@ fn element_to_tokens(
|
||||
Some(component_to_tokens(cx, node, global_class))
|
||||
}
|
||||
} else {
|
||||
let tag = node.name.to_string();
|
||||
let tag = name.to_string();
|
||||
let name = if is_custom_element(&tag) {
|
||||
let name = node.name.to_string();
|
||||
let name = node.name().to_string();
|
||||
quote! { leptos::leptos_dom::html::custom(#cx, leptos::leptos_dom::html::Custom::new(#name)) }
|
||||
} else if is_svg_element(&tag) {
|
||||
let name = &node.name;
|
||||
parent_type = TagType::Svg;
|
||||
quote! { leptos::leptos_dom::svg::#name(#cx) }
|
||||
} else if is_math_ml_element(&tag) {
|
||||
let name = &node.name;
|
||||
parent_type = TagType::Math;
|
||||
quote! { leptos::leptos_dom::math::#name(#cx) }
|
||||
} else if is_ambiguous_element(&tag) {
|
||||
let name = &node.name;
|
||||
match parent_type {
|
||||
TagType::Unknown => {
|
||||
// We decided this warning was too aggressive, but I'll leave it here in case we want it later
|
||||
@@ -1020,12 +1020,11 @@ fn element_to_tokens(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let name = &node.name;
|
||||
parent_type = TagType::Html;
|
||||
quote! { leptos::leptos_dom::html::#name(#cx) }
|
||||
};
|
||||
let attrs = node.attributes.iter().filter_map(|node| {
|
||||
if let Node::Attribute(node) = node {
|
||||
let attrs = node.attributes().iter().filter_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
let name = node.key.to_string();
|
||||
let name = name.trim();
|
||||
if name.starts_with("class:")
|
||||
@@ -1041,8 +1040,8 @@ fn element_to_tokens(
|
||||
None
|
||||
}
|
||||
});
|
||||
let class_attrs = node.attributes.iter().filter_map(|node| {
|
||||
if let Node::Attribute(node) = node {
|
||||
let class_attrs = node.attributes().iter().filter_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
let name = node.key.to_string();
|
||||
if let Some((fancy, _, _)) = fancy_class_name(&name, cx, node) {
|
||||
Some(fancy)
|
||||
@@ -1055,8 +1054,8 @@ fn element_to_tokens(
|
||||
None
|
||||
}
|
||||
});
|
||||
let style_attrs = node.attributes.iter().filter_map(|node| {
|
||||
if let Node::Attribute(node) = node {
|
||||
let style_attrs = node.attributes().iter().filter_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
let name = node.key.to_string();
|
||||
if let Some((fancy, _, _)) = fancy_style_name(&name, cx, node) {
|
||||
Some(fancy)
|
||||
@@ -1101,32 +1100,18 @@ fn element_to_tokens(
|
||||
}),
|
||||
false,
|
||||
),
|
||||
Node::Text(node) => {
|
||||
if let Some(primitive) = value_to_string(&node.value) {
|
||||
(quote! { #primitive }, true)
|
||||
} else {
|
||||
let value = node.value.as_ref();
|
||||
(
|
||||
quote! {
|
||||
#[allow(unused_braces)] #value
|
||||
},
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
Node::Block(node) => {
|
||||
if let Some(primitive) = value_to_string(&node.value) {
|
||||
(quote! { #primitive }, true)
|
||||
} else {
|
||||
let value = node.value.as_ref();
|
||||
(
|
||||
quote! {
|
||||
#[allow(unused_braces)] #value
|
||||
},
|
||||
false,
|
||||
)
|
||||
}
|
||||
Node::Text(node) => (quote! { #node }, true),
|
||||
Node::RawText(node) => {
|
||||
let text = node.to_string_best();
|
||||
let text = syn::LitStr::new(&text, node.span());
|
||||
(quote! { #text }, true)
|
||||
}
|
||||
Node::Block(node) => (
|
||||
quote! {
|
||||
#node
|
||||
},
|
||||
false,
|
||||
),
|
||||
Node::Element(node) => (
|
||||
element_to_tokens(
|
||||
cx,
|
||||
@@ -1139,9 +1124,7 @@ fn element_to_tokens(
|
||||
.unwrap_or_default(),
|
||||
false,
|
||||
),
|
||||
Node::Comment(_) | Node::Doctype(_) | Node::Attribute(_) => {
|
||||
(quote! {}, false)
|
||||
}
|
||||
Node::Comment(_) | Node::Doctype(_) => (quote! {}, false),
|
||||
};
|
||||
if is_static {
|
||||
quote! {
|
||||
@@ -1172,7 +1155,7 @@ fn element_to_tokens(
|
||||
|
||||
fn attribute_to_tokens(
|
||||
cx: &Ident,
|
||||
node: &NodeAttribute,
|
||||
node: &KeyedAttribute,
|
||||
global_class: Option<&TokenTree>,
|
||||
) -> TokenStream {
|
||||
let span = node.key.span();
|
||||
@@ -1303,7 +1286,7 @@ fn attribute_to_tokens(
|
||||
// special case of global_class and class attribute
|
||||
if name == "class"
|
||||
&& global_class.is_some()
|
||||
&& node.value.as_ref().and_then(value_to_string).is_none()
|
||||
&& node.value().and_then(value_to_string).is_none()
|
||||
{
|
||||
let span = node.key.span();
|
||||
proc_macro_error::emit_error!(span, "Combining a global class (view! { cx, class = ... }) \
|
||||
@@ -1313,10 +1296,8 @@ fn attribute_to_tokens(
|
||||
};
|
||||
|
||||
// all other attributes
|
||||
let value = match node.value.as_ref() {
|
||||
let value = match node.value() {
|
||||
Some(value) => {
|
||||
let value = value.as_ref();
|
||||
|
||||
quote! { #value }
|
||||
}
|
||||
None => quote_spanned! { span => "" },
|
||||
@@ -1367,7 +1348,7 @@ pub(crate) fn parse_event_name(name: &str) -> (TokenStream, bool, bool) {
|
||||
pub(crate) fn slot_to_tokens(
|
||||
cx: &Ident,
|
||||
node: &NodeElement,
|
||||
slot: &NodeAttribute,
|
||||
slot: &KeyedAttribute,
|
||||
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
|
||||
global_class: Option<&TokenTree>,
|
||||
) {
|
||||
@@ -1376,19 +1357,19 @@ pub(crate) fn slot_to_tokens(
|
||||
let name = convert_to_snake_case(if name.starts_with("slot:") {
|
||||
name.replacen("slot:", "", 1)
|
||||
} else {
|
||||
node.name.to_string()
|
||||
node.name().to_string()
|
||||
});
|
||||
|
||||
let component_name = ident_from_tag_name(&node.name);
|
||||
let span = node.name.span();
|
||||
let component_name = ident_from_tag_name(node.name());
|
||||
let span = node.name().span();
|
||||
|
||||
let Some(parent_slots) = parent_slots else {
|
||||
proc_macro_error::emit_error!(span, "slots cannot be used inside HTML elements");
|
||||
return;
|
||||
};
|
||||
|
||||
let attrs = node.attributes.iter().filter_map(|node| {
|
||||
if let Node::Attribute(node) = node {
|
||||
let attrs = node.attributes().iter().filter_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
if is_slot(node) {
|
||||
None
|
||||
} else {
|
||||
@@ -1406,10 +1387,8 @@ pub(crate) fn slot_to_tokens(
|
||||
let name = &attr.key;
|
||||
|
||||
let value = attr
|
||||
.value
|
||||
.as_ref()
|
||||
.value()
|
||||
.map(|v| {
|
||||
let v = v.as_ref();
|
||||
quote! { #v }
|
||||
})
|
||||
.unwrap_or_else(|| quote! { #name });
|
||||
@@ -1474,9 +1453,9 @@ pub(crate) fn slot_to_tokens(
|
||||
let slot = Ident::new(&slot, span);
|
||||
if values.len() > 1 {
|
||||
quote! {
|
||||
.#slot(vec![
|
||||
.#slot([
|
||||
#(#values)*
|
||||
])
|
||||
].to_vec())
|
||||
}
|
||||
} else {
|
||||
let value = &values[0];
|
||||
@@ -1504,12 +1483,12 @@ pub(crate) fn component_to_tokens(
|
||||
node: &NodeElement,
|
||||
global_class: Option<&TokenTree>,
|
||||
) -> TokenStream {
|
||||
let name = &node.name;
|
||||
let component_name = ident_from_tag_name(&node.name);
|
||||
let span = node.name.span();
|
||||
let name = node.name();
|
||||
let component_name = ident_from_tag_name(node.name());
|
||||
let span = node.name().span();
|
||||
|
||||
let attrs = node.attributes.iter().filter_map(|node| {
|
||||
if let Node::Attribute(node) = node {
|
||||
let attrs = node.attributes().iter().filter_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
Some(node)
|
||||
} else {
|
||||
None
|
||||
@@ -1526,10 +1505,8 @@ pub(crate) fn component_to_tokens(
|
||||
let name = &attr.key;
|
||||
|
||||
let value = attr
|
||||
.value
|
||||
.as_ref()
|
||||
.value()
|
||||
.map(|v| {
|
||||
let v = v.as_ref();
|
||||
quote! { #v }
|
||||
})
|
||||
.unwrap_or_else(|| quote! { #name });
|
||||
@@ -1637,7 +1614,7 @@ pub(crate) fn component_to_tokens(
|
||||
}
|
||||
|
||||
pub(crate) fn event_from_attribute_node(
|
||||
attr: &NodeAttribute,
|
||||
attr: &KeyedAttribute,
|
||||
force_undelegated: bool,
|
||||
) -> (TokenStream, &Expr) {
|
||||
let event_name = attr
|
||||
@@ -1697,7 +1674,7 @@ fn ident_from_tag_name(tag_name: &NodeName) -> Ident {
|
||||
fn expr_to_ident(expr: &syn::Expr) -> Option<&ExprPath> {
|
||||
match expr {
|
||||
syn::Expr::Block(block) => block.block.stmts.last().and_then(|stmt| {
|
||||
if let syn::Stmt::Expr(expr) = stmt {
|
||||
if let syn::Stmt::Expr(expr, ..) = stmt {
|
||||
expr_to_ident(expr)
|
||||
} else {
|
||||
None
|
||||
@@ -1708,15 +1685,15 @@ fn expr_to_ident(expr: &syn::Expr) -> Option<&ExprPath> {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_slot(node: &NodeAttribute) -> bool {
|
||||
fn is_slot(node: &KeyedAttribute) -> bool {
|
||||
let key = node.key.to_string();
|
||||
let key = key.trim();
|
||||
key == "slot" || key.starts_with("slot:")
|
||||
}
|
||||
|
||||
fn get_slot(node: &NodeElement) -> Option<&NodeAttribute> {
|
||||
node.attributes.iter().find_map(|node| {
|
||||
if let Node::Attribute(node) = node {
|
||||
fn get_slot(node: &NodeElement) -> Option<&KeyedAttribute> {
|
||||
node.attributes().iter().find_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
if is_slot(node) {
|
||||
Some(node)
|
||||
} else {
|
||||
@@ -1744,7 +1721,7 @@ fn is_self_closing(node: &NodeElement) -> bool {
|
||||
// self-closing tags
|
||||
// https://developer.mozilla.org/en-US/docs/Glossary/Empty_element
|
||||
matches!(
|
||||
node.name.to_string().as_str(),
|
||||
node.name().to_string().as_str(),
|
||||
"area"
|
||||
| "base"
|
||||
| "br"
|
||||
@@ -1899,13 +1876,13 @@ fn parse_event(event_name: &str) -> (&str, bool) {
|
||||
fn fancy_class_name<'a>(
|
||||
name: &str,
|
||||
cx: &Ident,
|
||||
node: &'a NodeAttribute,
|
||||
node: &'a KeyedAttribute,
|
||||
) -> Option<(TokenStream, String, &'a Expr)> {
|
||||
// special case for complex class names:
|
||||
// e.g., Tailwind `class=("mt-[calc(100vh_-_3rem)]", true)`
|
||||
if name == "class" {
|
||||
if let Some(expr) = node.value.as_ref() {
|
||||
if let syn::Expr::Tuple(tuple) = expr.as_ref() {
|
||||
if let Some(expr) = node.value() {
|
||||
if let syn::Expr::Tuple(tuple) = expr {
|
||||
if tuple.elems.len() == 2 {
|
||||
let span = node.key.span();
|
||||
let class = quote_spanned! {
|
||||
@@ -1948,12 +1925,12 @@ fn fancy_class_name<'a>(
|
||||
fn fancy_style_name<'a>(
|
||||
name: &str,
|
||||
cx: &Ident,
|
||||
node: &'a NodeAttribute,
|
||||
node: &'a KeyedAttribute,
|
||||
) -> Option<(TokenStream, String, &'a Expr)> {
|
||||
// special case for complex dynamic style names:
|
||||
if name == "style" {
|
||||
if let Some(expr) = node.value.as_ref() {
|
||||
if let syn::Expr::Tuple(tuple) = expr.as_ref() {
|
||||
if let Some(expr) = node.value() {
|
||||
if let syn::Expr::Tuple(tuple) = expr {
|
||||
if tuple.elems.len() == 2 {
|
||||
let span = node.key.span();
|
||||
let style = quote_spanned! {
|
||||
|
||||
@@ -44,7 +44,7 @@ error: unexpected end of input, expected assignment `=`
|
||||
47 | #[prop(default)] default: bool,
|
||||
| ^
|
||||
|
||||
error: unexpected end of input, expected one of: `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
|
||||
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:56:22
|
||||
|
||||
@@ -44,7 +44,7 @@ error: unexpected end of input, expected assignment `=`
|
||||
45 | #[prop(default)] default: bool,
|
||||
| ^
|
||||
|
||||
error: unexpected end of input, expected one of: `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
|
||||
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_absolute.rs:54:22
|
||||
|
||||
@@ -110,7 +110,7 @@ pub use slice::*;
|
||||
pub use spawn::*;
|
||||
pub use spawn_microtask::*;
|
||||
pub use stored_value::*;
|
||||
pub use suspense::SuspenseContext;
|
||||
pub use suspense::{GlobalSuspenseContext, SuspenseContext};
|
||||
pub use trigger::*;
|
||||
|
||||
mod macros {
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{
|
||||
SignalDispose, SignalGet, SignalGetUntracked, SignalStream, SignalWith,
|
||||
SignalWithUntracked,
|
||||
};
|
||||
use std::{any::Any, cell::RefCell, fmt::Debug, marker::PhantomData, rc::Rc};
|
||||
use std::{any::Any, cell::RefCell, fmt, marker::PhantomData, rc::Rc};
|
||||
|
||||
/// Creates an efficient derived reactive value based on other reactive values.
|
||||
///
|
||||
@@ -151,7 +151,6 @@ where
|
||||
/// });
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Memo<T>
|
||||
where
|
||||
T: 'static,
|
||||
@@ -180,6 +179,26 @@ where
|
||||
|
||||
impl<T> Copy for Memo<T> {}
|
||||
|
||||
impl<T> fmt::Debug for Memo<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut s = f.debug_struct("Memo");
|
||||
s.field("runtime", &self.runtime);
|
||||
s.field("id", &self.id);
|
||||
s.field("ty", &self.ty);
|
||||
#[cfg(any(debug_assertions, feature = "ssr"))]
|
||||
s.field("defined_at", &self.defined_at);
|
||||
s.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Eq for Memo<T> {}
|
||||
|
||||
impl<T> PartialEq for Memo<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.runtime == other.runtime && self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> SignalGetUntracked<T> for Memo<T> {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
|
||||
@@ -5,8 +5,9 @@ use crate::{
|
||||
runtime::{with_runtime, RuntimeId},
|
||||
serialization::Serializable,
|
||||
spawn::spawn_local,
|
||||
use_context, Memo, ReadSignal, Scope, ScopeProperty, SignalGetUntracked,
|
||||
SignalSet, SignalUpdate, SignalWith, SuspenseContext, WriteSignal,
|
||||
use_context, GlobalSuspenseContext, Memo, ReadSignal, Scope, ScopeProperty,
|
||||
SignalGetUntracked, SignalSet, SignalUpdate, SignalWith, SuspenseContext,
|
||||
WriteSignal,
|
||||
};
|
||||
use std::{
|
||||
any::Any,
|
||||
@@ -820,6 +821,7 @@ where
|
||||
f: impl FnOnce(&T) -> U,
|
||||
location: &'static Location<'static>,
|
||||
) -> Option<U> {
|
||||
let global_suspense_cx = use_context::<GlobalSuspenseContext>(cx);
|
||||
let suspense_cx = use_context::<SuspenseContext>(cx);
|
||||
|
||||
let v = self
|
||||
@@ -882,6 +884,24 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(g) = &global_suspense_cx {
|
||||
if let Ok(ref mut contexts) = suspense_contexts.try_borrow_mut()
|
||||
{
|
||||
g.with_inner(|s| {
|
||||
if !contexts.contains(s) {
|
||||
contexts.insert(*s);
|
||||
|
||||
if !has_value {
|
||||
s.increment(
|
||||
serializable
|
||||
!= ResourceSerialization::Local,
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
create_isomorphic_effect(cx, increment);
|
||||
@@ -1005,6 +1025,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum AnyResource {
|
||||
Unserializable(Rc<dyn UnserializableResource>),
|
||||
Serializable(Rc<dyn SerializableResource>),
|
||||
|
||||
@@ -743,7 +743,7 @@ impl Runtime {
|
||||
S: 'static,
|
||||
T: 'static,
|
||||
{
|
||||
let resources = self.resources.borrow();
|
||||
let resources = { self.resources.borrow().clone() };
|
||||
let res = resources.get(id);
|
||||
if let Some(res) = res {
|
||||
let res_state = match res {
|
||||
@@ -796,7 +796,8 @@ impl Runtime {
|
||||
cx: Scope,
|
||||
) -> FuturesUnordered<PinnedFuture<(ResourceId, String)>> {
|
||||
let f = FuturesUnordered::new();
|
||||
for (id, resource) in self.resources.borrow().iter() {
|
||||
let resources = { self.resources.borrow().clone() };
|
||||
for (id, resource) in resources.iter() {
|
||||
if let AnyResource::Serializable(resource) = resource {
|
||||
f.push(resource.to_serialization_resolver(cx, id));
|
||||
}
|
||||
|
||||
@@ -10,7 +10,13 @@ use crate::{
|
||||
};
|
||||
use futures::Stream;
|
||||
use std::{
|
||||
any::Any, cell::RefCell, fmt::Debug, marker::PhantomData, pin::Pin, rc::Rc,
|
||||
any::Any,
|
||||
cell::RefCell,
|
||||
fmt,
|
||||
hash::{Hash, Hasher},
|
||||
marker::PhantomData,
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -463,7 +469,6 @@ pub fn create_signal_from_stream<T>(
|
||||
/// # }).dispose();
|
||||
/// #
|
||||
/// ```
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ReadSignal<T>
|
||||
where
|
||||
T: 'static,
|
||||
@@ -784,6 +789,33 @@ impl<T> Clone for ReadSignal<T> {
|
||||
|
||||
impl<T> Copy for ReadSignal<T> {}
|
||||
|
||||
impl<T> fmt::Debug for ReadSignal<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut s = f.debug_struct("ReadSignal");
|
||||
s.field("runtime", &self.runtime);
|
||||
s.field("id", &self.id);
|
||||
s.field("ty", &self.ty);
|
||||
#[cfg(any(debug_assertions, feature = "ssr"))]
|
||||
s.field("defined_at", &self.defined_at);
|
||||
s.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Eq for ReadSignal<T> {}
|
||||
|
||||
impl<T> PartialEq for ReadSignal<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.runtime == other.runtime && self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Hash for ReadSignal<T> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.runtime.hash(state);
|
||||
self.id.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
/// The setter for a reactive signal.
|
||||
///
|
||||
/// A signal is a piece of data that may change over time,
|
||||
@@ -829,7 +861,6 @@ impl<T> Copy for ReadSignal<T> {}
|
||||
/// # }).dispose();
|
||||
/// #
|
||||
/// ```
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct WriteSignal<T>
|
||||
where
|
||||
T: 'static,
|
||||
@@ -1045,6 +1076,33 @@ impl<T> Clone for WriteSignal<T> {
|
||||
|
||||
impl<T> Copy for WriteSignal<T> {}
|
||||
|
||||
impl<T> fmt::Debug for WriteSignal<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut s = f.debug_struct("WriteSignal");
|
||||
s.field("runtime", &self.runtime);
|
||||
s.field("id", &self.id);
|
||||
s.field("ty", &self.ty);
|
||||
#[cfg(any(debug_assertions, feature = "ssr"))]
|
||||
s.field("defined_at", &self.defined_at);
|
||||
s.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Eq for WriteSignal<T> {}
|
||||
|
||||
impl<T> PartialEq for WriteSignal<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.runtime == other.runtime && self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Hash for WriteSignal<T> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.runtime.hash(state);
|
||||
self.id.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a reactive signal with the getter and setter unified in one value.
|
||||
/// You may prefer this style, or it may be easier to pass around in a context
|
||||
/// or as a function argument.
|
||||
@@ -1126,7 +1184,6 @@ pub fn create_rw_signal<T>(cx: Scope, value: T) -> RwSignal<T> {
|
||||
/// # }).dispose();
|
||||
/// #
|
||||
/// ```
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct RwSignal<T>
|
||||
where
|
||||
T: 'static,
|
||||
@@ -1146,6 +1203,33 @@ impl<T> Clone for RwSignal<T> {
|
||||
|
||||
impl<T> Copy for RwSignal<T> {}
|
||||
|
||||
impl<T> fmt::Debug for RwSignal<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut s = f.debug_struct("RwSignal");
|
||||
s.field("runtime", &self.runtime);
|
||||
s.field("id", &self.id);
|
||||
s.field("ty", &self.ty);
|
||||
#[cfg(any(debug_assertions, feature = "ssr"))]
|
||||
s.field("defined_at", &self.defined_at);
|
||||
s.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Eq for RwSignal<T> {}
|
||||
|
||||
impl<T> PartialEq for RwSignal<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.runtime == other.runtime && self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Hash for RwSignal<T> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.runtime.hash(state);
|
||||
self.id.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> SignalGetUntracked<T> for RwSignal<T> {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
|
||||
@@ -60,7 +60,6 @@ where
|
||||
/// assert_eq!(above_3(&memoized_double_count.into()), true);
|
||||
/// # });
|
||||
/// ```
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Signal<T>
|
||||
where
|
||||
T: 'static,
|
||||
@@ -82,6 +81,24 @@ impl<T> Clone for Signal<T> {
|
||||
|
||||
impl<T> Copy for Signal<T> {}
|
||||
|
||||
impl<T> std::fmt::Debug for Signal<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut s = f.debug_struct("Signal");
|
||||
s.field("inner", &self.inner);
|
||||
#[cfg(any(debug_assertions, feature = "ssr"))]
|
||||
s.field("defined_at", &self.defined_at);
|
||||
s.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Eq for Signal<T> {}
|
||||
|
||||
impl<T> PartialEq for Signal<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.inner == other.inner
|
||||
}
|
||||
}
|
||||
|
||||
/// Please note that using `Signal::with_untracked` still clones the inner value,
|
||||
/// so there's no benefit to using it as opposed to calling
|
||||
/// `Signal::get_untracked`.
|
||||
@@ -431,10 +448,7 @@ impl<T> Clone for SignalTypes<T> {
|
||||
|
||||
impl<T> Copy for SignalTypes<T> {}
|
||||
|
||||
impl<T> std::fmt::Debug for SignalTypes<T>
|
||||
where
|
||||
T: std::fmt::Debug,
|
||||
{
|
||||
impl<T> std::fmt::Debug for SignalTypes<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::ReadSignal(arg0) => {
|
||||
@@ -448,10 +462,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PartialEq for SignalTypes<T>
|
||||
where
|
||||
T: PartialEq,
|
||||
{
|
||||
impl<T> PartialEq for SignalTypes<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::ReadSignal(l0), Self::ReadSignal(r0)) => l0 == r0,
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
#![forbid(unsafe_code)]
|
||||
use crate::{with_runtime, RuntimeId, Scope, ScopeProperty};
|
||||
use std::{cell::RefCell, marker::PhantomData, rc::Rc};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
fmt,
|
||||
hash::{Hash, Hasher},
|
||||
marker::PhantomData,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
slotmap::new_key_type! {
|
||||
/// Unique ID assigned to a [`StoredValue`].
|
||||
@@ -16,7 +22,6 @@ slotmap::new_key_type! {
|
||||
/// and [`RwSignal`](crate::RwSignal)), it is `Copy` and `'static`. Unlike the signal
|
||||
/// types, it is not reactive; accessing it does not cause effects to subscribe, and
|
||||
/// updating it does not notify anything else.
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct StoredValue<T>
|
||||
where
|
||||
T: 'static,
|
||||
@@ -38,6 +43,31 @@ impl<T> Clone for StoredValue<T> {
|
||||
|
||||
impl<T> Copy for StoredValue<T> {}
|
||||
|
||||
impl<T> fmt::Debug for StoredValue<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("StoredValue")
|
||||
.field("runtime", &self.runtime)
|
||||
.field("id", &self.id)
|
||||
.field("ty", &self.ty)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Eq for StoredValue<T> {}
|
||||
|
||||
impl<T> PartialEq for StoredValue<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.runtime == other.runtime && self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Hash for StoredValue<T> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.runtime.hash(state);
|
||||
self.id.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> StoredValue<T> {
|
||||
/// Returns a clone of the current stored value.
|
||||
///
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
use crate::{
|
||||
create_rw_signal, create_signal, queue_microtask, store_value, ReadSignal,
|
||||
RwSignal, Scope, SignalUpdate, StoredValue, WriteSignal,
|
||||
create_isomorphic_effect, create_rw_signal, create_signal, queue_microtask,
|
||||
signal::SignalGet, store_value, ReadSignal, RwSignal, Scope, SignalSet,
|
||||
SignalUpdate, StoredValue, WriteSignal,
|
||||
};
|
||||
use futures::Future;
|
||||
use std::{borrow::Cow, collections::VecDeque, pin::Pin};
|
||||
use std::{
|
||||
borrow::Cow, cell::RefCell, collections::VecDeque, pin::Pin, rc::Rc,
|
||||
};
|
||||
|
||||
/// Tracks [`Resource`](crate::Resource)s that are read under a suspense context,
|
||||
/// i.e., within a [`Suspense`](https://docs.rs/leptos_core/latest/leptos_core/fn.Suspense.html) component.
|
||||
@@ -20,6 +23,30 @@ pub struct SuspenseContext {
|
||||
pub(crate) should_block: StoredValue<bool>,
|
||||
}
|
||||
|
||||
/// A single, global suspense context that will be checked when resources
|
||||
/// are read. This won’t be “blocked” by lower suspense components. This is
|
||||
/// useful for e.g., holding route transitions.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GlobalSuspenseContext(Rc<RefCell<SuspenseContext>>);
|
||||
|
||||
impl GlobalSuspenseContext {
|
||||
/// Creates an empty global suspense context.
|
||||
pub fn new(cx: Scope) -> Self {
|
||||
Self(Rc::new(RefCell::new(SuspenseContext::new(cx))))
|
||||
}
|
||||
|
||||
/// Runs a function with a reference to the underlying suspense context.
|
||||
pub fn with_inner<T>(&self, f: impl FnOnce(&SuspenseContext) -> T) -> T {
|
||||
f(&*self.0.borrow())
|
||||
}
|
||||
|
||||
/// Runs a function with a reference to the underlying suspense context.
|
||||
pub fn reset(&self, cx: Scope) {
|
||||
let mut inner = self.0.borrow_mut();
|
||||
_ = std::mem::replace(&mut *inner, SuspenseContext::new(cx));
|
||||
}
|
||||
}
|
||||
|
||||
impl SuspenseContext {
|
||||
/// Whether the suspense contains local resources at this moment,
|
||||
/// and therefore can't be serialized
|
||||
@@ -32,6 +59,25 @@ impl SuspenseContext {
|
||||
pub fn should_block(&self) -> bool {
|
||||
self.should_block.get_value()
|
||||
}
|
||||
|
||||
/// Returns a `Future` that resolves when this suspense is resolved.
|
||||
pub fn to_future(&self, cx: Scope) -> impl Future<Output = ()> {
|
||||
use futures::StreamExt;
|
||||
|
||||
let pending_resources = self.pending_resources;
|
||||
let (tx, mut rx) = futures::channel::mpsc::channel(1);
|
||||
let tx = RefCell::new(tx);
|
||||
queue_microtask(move || {
|
||||
create_isomorphic_effect(cx, move |_| {
|
||||
if pending_resources.get() == 0 {
|
||||
_ = tx.borrow_mut().try_send(());
|
||||
}
|
||||
})
|
||||
});
|
||||
async move {
|
||||
rx.next().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for SuspenseContext {
|
||||
@@ -98,6 +144,12 @@ impl SuspenseContext {
|
||||
});
|
||||
}
|
||||
|
||||
/// Resets the counter of pending resources.
|
||||
pub fn clear(&self) {
|
||||
self.set_pending_resources.set(0);
|
||||
self.pending_serializable_resources.set(0);
|
||||
}
|
||||
|
||||
/// Tests whether all of the pending resources have resolved.
|
||||
pub fn ready(&self) -> bool {
|
||||
self.pending_resources
|
||||
|
||||
@@ -134,8 +134,14 @@ where
|
||||
request_animation_frame(move || {
|
||||
if let Err(e) = navigate(
|
||||
&format!(
|
||||
"{}{}",
|
||||
url.pathname, url.search,
|
||||
"{}{}{}",
|
||||
url.pathname,
|
||||
if url.search.is_empty() {
|
||||
""
|
||||
} else {
|
||||
"?"
|
||||
},
|
||||
url.search,
|
||||
),
|
||||
Default::default(),
|
||||
) {
|
||||
@@ -190,8 +196,14 @@ where
|
||||
request_animation_frame(move || {
|
||||
if let Err(e) = navigate(
|
||||
&format!(
|
||||
"{}{}",
|
||||
url.pathname, url.search,
|
||||
"{}{}{}",
|
||||
url.pathname,
|
||||
if url.search.is_empty() {
|
||||
""
|
||||
} else {
|
||||
"?"
|
||||
},
|
||||
url.search,
|
||||
),
|
||||
Default::default(),
|
||||
) {
|
||||
|
||||
@@ -90,10 +90,14 @@ where
|
||||
children: Children,
|
||||
) -> HtmlElement<leptos::html::A> {
|
||||
#[cfg(not(any(feature = "hydrate", feature = "csr")))]
|
||||
_ = state;
|
||||
{
|
||||
_ = state;
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "hydrate", feature = "csr")))]
|
||||
_ = replace;
|
||||
{
|
||||
_ = replace;
|
||||
}
|
||||
|
||||
let location = use_location(cx);
|
||||
let is_active = create_memo(cx, move |_| match href.get() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod form;
|
||||
mod link;
|
||||
mod outlet;
|
||||
mod progress;
|
||||
mod redirect;
|
||||
mod route;
|
||||
mod router;
|
||||
@@ -9,6 +10,7 @@ mod routes;
|
||||
pub use form::*;
|
||||
pub use link::*;
|
||||
pub use outlet::*;
|
||||
pub use progress::*;
|
||||
pub use redirect::*;
|
||||
pub use route::*;
|
||||
pub use router::*;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
animation::{Animation, AnimationState},
|
||||
use_is_back_navigation, use_route,
|
||||
use_is_back_navigation, use_route, SetIsRouting,
|
||||
};
|
||||
use leptos::{leptos_dom::HydrationCtx, *};
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
@@ -45,6 +45,43 @@ pub fn Outlet(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
});
|
||||
|
||||
let outlet: Signal<Option<View>> =
|
||||
if cfg!(any(feature = "csr", feature = "hydrate"))
|
||||
&& use_context::<SetIsRouting>(cx).is_some()
|
||||
{
|
||||
let global_suspense = expect_context::<GlobalSuspenseContext>(cx);
|
||||
|
||||
let (current_view, set_current_view) = create_signal(cx, None);
|
||||
|
||||
create_effect(cx, {
|
||||
let global_suspense = global_suspense.clone();
|
||||
move |prev| {
|
||||
let outlet = outlet.get();
|
||||
let is_fallback =
|
||||
!global_suspense.with_inner(SuspenseContext::ready);
|
||||
if prev.is_none() {
|
||||
set_current_view.set(outlet);
|
||||
} else if !is_fallback {
|
||||
queue_microtask({
|
||||
let global_suspense = global_suspense.clone();
|
||||
move || {
|
||||
let is_fallback = cx.untrack(move || {
|
||||
!global_suspense
|
||||
.with_inner(SuspenseContext::ready)
|
||||
});
|
||||
if !is_fallback {
|
||||
set_current_view.set(outlet);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
current_view.into()
|
||||
} else {
|
||||
outlet.into()
|
||||
};
|
||||
|
||||
leptos::leptos_dom::DynChild::new_with_id(id, move || outlet.get())
|
||||
}
|
||||
|
||||
|
||||
69
router/src/components/progress.rs
Normal file
69
router/src/components/progress.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use leptos::{leptos_dom::helpers::IntervalHandle, *};
|
||||
|
||||
/// A visible indicator that the router is in the process of navigating
|
||||
/// to another route.
|
||||
///
|
||||
/// This is used when `<Router set_is_routing>` has been provided, to
|
||||
/// provide some visual indicator that the page is currently loading
|
||||
/// async data, so that it is does not appear to have frozen. It can be
|
||||
/// styled independently.
|
||||
#[component]
|
||||
pub fn RoutingProgress(
|
||||
cx: Scope,
|
||||
/// Whether the router is currently loading the new page.
|
||||
#[prop(into)]
|
||||
is_routing: Signal<bool>,
|
||||
/// The maximum expected time for loading, which is used to
|
||||
/// calibrate the animation process.
|
||||
#[prop(optional, into)]
|
||||
max_time: std::time::Duration,
|
||||
/// The time to show the full progress bar after page has loaded, before hiding it. (Defaults to 100ms.)
|
||||
#[prop(default = std::time::Duration::from_millis(250))]
|
||||
before_hiding: std::time::Duration,
|
||||
/// CSS classes to be applied to the `<progress>`.
|
||||
#[prop(optional, into)]
|
||||
class: String,
|
||||
) -> impl IntoView {
|
||||
const INCREMENT_EVERY_MS: f32 = 5.0;
|
||||
let expected_increments =
|
||||
max_time.as_secs_f32() / (INCREMENT_EVERY_MS / 1000.0);
|
||||
let percent_per_increment = 100.0 / expected_increments;
|
||||
|
||||
let (is_showing, set_is_showing) = create_signal(cx, false);
|
||||
let (progress, set_progress) = create_signal(cx, 0.0);
|
||||
|
||||
create_effect(cx, move |prev: Option<Option<IntervalHandle>>| {
|
||||
if is_routing.get() && !is_showing.get() {
|
||||
set_is_showing.set(true);
|
||||
set_interval_with_handle(
|
||||
move || {
|
||||
set_progress.update(|n| *n += percent_per_increment);
|
||||
},
|
||||
std::time::Duration::from_millis(INCREMENT_EVERY_MS as u64),
|
||||
)
|
||||
.ok()
|
||||
} else if is_routing.get() && is_showing.get() {
|
||||
set_progress.set(0.0);
|
||||
prev?
|
||||
} else {
|
||||
set_progress.set(100.0);
|
||||
set_timeout(
|
||||
move || {
|
||||
set_progress.set(0.0);
|
||||
set_is_showing.set(false);
|
||||
},
|
||||
before_hiding,
|
||||
);
|
||||
if let Some(Some(interval)) = prev {
|
||||
interval.clear();
|
||||
}
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
<Show when=move || is_showing.get() fallback=|_| ()>
|
||||
<progress class=class.clone() min="0" max="100" value=move || progress.get()/>
|
||||
</Show>
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,9 @@ pub fn Router(
|
||||
/// A fallback that should be shown if no route is matched.
|
||||
#[prop(optional)]
|
||||
fallback: Option<fn(Scope) -> View>,
|
||||
/// A signal that will be set while the navigation process is underway.
|
||||
#[prop(optional, into)]
|
||||
set_is_routing: Option<SignalSetter<bool>>,
|
||||
/// The `<Router/>` should usually wrap your whole page. It can contain
|
||||
/// any elements, and should include a [Routes](crate::Routes) component somewhere
|
||||
/// to define and display [Route](crate::Route)s.
|
||||
@@ -32,10 +35,17 @@ pub fn Router(
|
||||
// create a new RouterContext and provide it to every component beneath the router
|
||||
let router = RouterContext::new(cx, base, fallback);
|
||||
provide_context(cx, router);
|
||||
provide_context(cx, GlobalSuspenseContext::new(cx));
|
||||
if let Some(set_is_routing) = set_is_routing {
|
||||
provide_context(cx, SetIsRouting(set_is_routing));
|
||||
}
|
||||
|
||||
children(cx)
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct SetIsRouting(pub SignalSetter<bool>);
|
||||
|
||||
/// Context type that contains information about the current router state.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RouterContext {
|
||||
@@ -228,6 +238,9 @@ impl RouterContextInner {
|
||||
resolve_path("", to, None).map(String::from)
|
||||
};
|
||||
|
||||
// reset count of pending resources at global level
|
||||
expect_context::<GlobalSuspenseContext>(cx).reset(cx);
|
||||
|
||||
match resolved_to {
|
||||
None => Err(NavigationError::NotRoutable(to.to_string())),
|
||||
Some(resolved_to) => {
|
||||
@@ -262,18 +275,34 @@ impl RouterContextInner {
|
||||
move |state| *state = next_state
|
||||
});
|
||||
|
||||
self.path_stack.update_value(|stack| {
|
||||
let global_suspense =
|
||||
expect_context::<GlobalSuspenseContext>(cx);
|
||||
let path_stack = self.path_stack;
|
||||
path_stack.update_value(|stack| {
|
||||
stack.push(resolved_to.clone())
|
||||
});
|
||||
|
||||
if referrers.borrow().len() == len {
|
||||
this.navigate_end(LocationChange {
|
||||
value: resolved_to,
|
||||
replace: false,
|
||||
scroll: true,
|
||||
state,
|
||||
})
|
||||
let set_is_routing = use_context::<SetIsRouting>(cx);
|
||||
if let Some(set_is_routing) = set_is_routing {
|
||||
set_is_routing.0.set(true);
|
||||
}
|
||||
spawn_local(async move {
|
||||
if let Some(set_is_routing) = set_is_routing {
|
||||
global_suspense
|
||||
.with_inner(|s| s.to_future(cx))
|
||||
.await;
|
||||
set_is_routing.0.set(false);
|
||||
}
|
||||
|
||||
if referrers.borrow().len() == len {
|
||||
this.navigate_end(LocationChange {
|
||||
value: resolved_to,
|
||||
replace: false,
|
||||
scroll: true,
|
||||
state,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -356,7 +385,10 @@ impl RouterContextInner {
|
||||
return;
|
||||
}
|
||||
|
||||
let to = path_name + &unescape(&url.search) + &unescape(&url.hash);
|
||||
let to = path_name
|
||||
+ if url.search.is_empty() { "" } else { "?" }
|
||||
+ &unescape(&url.search)
|
||||
+ &unescape(&url.hash);
|
||||
let state =
|
||||
leptos_dom::helpers::get_property(a.unchecked_ref(), "state")
|
||||
.ok()
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
expand_optionals, get_route_matches, join_paths, Branch, Matcher,
|
||||
RouteDefinition, RouteMatch,
|
||||
},
|
||||
use_is_back_navigation, RouteContext, RouterContext,
|
||||
use_is_back_navigation, RouteContext, RouterContext, SetIsRouting,
|
||||
};
|
||||
use leptos::{leptos_dom::HydrationCtx, *};
|
||||
use std::{
|
||||
@@ -56,6 +56,7 @@ pub fn Routes(
|
||||
|
||||
let id = HydrationCtx::id();
|
||||
let root = root_route(cx, base_route, route_states, root_equal);
|
||||
|
||||
leptos::leptos_dom::DynChild::new_with_id(id, move || root.get())
|
||||
.into_view(cx)
|
||||
}
|
||||
@@ -405,40 +406,75 @@ fn root_route(
|
||||
base_route: RouteContext,
|
||||
route_states: Memo<RouterState>,
|
||||
root_equal: Rc<Cell<bool>>,
|
||||
) -> Memo<Option<View>> {
|
||||
) -> Signal<Option<View>> {
|
||||
let root_cx = RefCell::new(None);
|
||||
|
||||
create_memo(cx, move |prev| {
|
||||
provide_context(cx, route_states);
|
||||
route_states.with(|state| {
|
||||
if state.routes.borrow().is_empty() {
|
||||
Some(base_route.outlet(cx).into_view(cx))
|
||||
} else {
|
||||
let root = state.routes.borrow();
|
||||
let root = root.get(0);
|
||||
if let Some(route) = root {
|
||||
provide_context(cx, route.clone());
|
||||
}
|
||||
|
||||
if prev.is_none() || !root_equal.get() {
|
||||
let (root_view, _) = cx.run_child_scope(|cx| {
|
||||
let prev_cx = std::mem::replace(
|
||||
&mut *root_cx.borrow_mut(),
|
||||
Some(cx),
|
||||
);
|
||||
if let Some(prev_cx) = prev_cx {
|
||||
prev_cx.dispose();
|
||||
}
|
||||
root.as_ref()
|
||||
.map(|route| route.outlet(cx).into_view(cx))
|
||||
});
|
||||
root_view
|
||||
let root_view = create_memo(cx, {
|
||||
let root_equal = Rc::clone(&root_equal);
|
||||
move |prev| {
|
||||
provide_context(cx, route_states);
|
||||
route_states.with(|state| {
|
||||
if state.routes.borrow().is_empty() {
|
||||
Some(base_route.outlet(cx).into_view(cx))
|
||||
} else {
|
||||
prev.cloned().unwrap()
|
||||
let root = state.routes.borrow();
|
||||
let root = root.get(0);
|
||||
if let Some(route) = root {
|
||||
provide_context(cx, route.clone());
|
||||
}
|
||||
|
||||
if prev.is_none() || !root_equal.get() {
|
||||
let (root_view, _) = cx.run_child_scope(|cx| {
|
||||
let prev_cx = std::mem::replace(
|
||||
&mut *root_cx.borrow_mut(),
|
||||
Some(cx),
|
||||
);
|
||||
if let Some(prev_cx) = prev_cx {
|
||||
prev_cx.dispose();
|
||||
}
|
||||
root.as_ref()
|
||||
.map(|route| route.outlet(cx).into_view(cx))
|
||||
});
|
||||
root_view
|
||||
} else {
|
||||
prev.cloned().unwrap()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
if cfg!(any(feature = "csr", feature = "hydrate"))
|
||||
&& use_context::<SetIsRouting>(cx).is_some()
|
||||
{
|
||||
let global_suspense = expect_context::<GlobalSuspenseContext>(cx);
|
||||
|
||||
let (current_view, set_current_view) = create_signal(cx, None);
|
||||
|
||||
create_effect(cx, move |prev| {
|
||||
let root = root_view.get();
|
||||
let is_fallback =
|
||||
!global_suspense.with_inner(SuspenseContext::ready);
|
||||
if prev.is_none() {
|
||||
set_current_view.set(root);
|
||||
} else if !is_fallback {
|
||||
queue_microtask({
|
||||
let global_suspense = global_suspense.clone();
|
||||
move || {
|
||||
let is_fallback = cx.untrack(move || {
|
||||
!global_suspense.with_inner(SuspenseContext::ready)
|
||||
});
|
||||
if !is_fallback {
|
||||
set_current_view.set(root);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
current_view.into()
|
||||
} else {
|
||||
root_view.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
|
||||
@@ -42,7 +42,11 @@ impl TryFrom<&str> for Url {
|
||||
Ok(Self {
|
||||
origin: url.origin(),
|
||||
pathname: url.pathname(),
|
||||
search: url.search(),
|
||||
search: url
|
||||
.search()
|
||||
.strip_prefix('?')
|
||||
.map(String::from)
|
||||
.unwrap_or_default(),
|
||||
search_params: ParamsMap(
|
||||
try_iter(&url.search_params())
|
||||
.map_js_error()?
|
||||
|
||||
@@ -76,6 +76,23 @@ pub fn use_resolved_path(
|
||||
}
|
||||
|
||||
/// Returns a function that can be used to navigate to a new route.
|
||||
///
|
||||
/// ## Panics
|
||||
/// `use_navigate` can sometimes panic due to a `BorrowMut` runtime error
|
||||
/// if it is called immediately during routing/rendering. In this case, you should
|
||||
/// wrap it in [`request_animation_frame`](leptos::request_animation_frame)
|
||||
/// to delay it until that routing process is complete.
|
||||
/// ```rust
|
||||
/// # use leptos::{request_animation_frame,create_scope,create_runtime};
|
||||
/// # create_scope(create_runtime(), |cx| {
|
||||
/// # if false { // can't actually navigate, no <Router/>
|
||||
/// let navigate = leptos_router::use_navigate(cx);
|
||||
/// request_animation_frame(move || {
|
||||
/// _ = navigate("/", Default::default());
|
||||
/// });
|
||||
/// # }
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn use_navigate(
|
||||
cx: Scope,
|
||||
) -> impl Fn(&str, NavigateOptions) -> Result<(), NavigationError> {
|
||||
@@ -84,7 +101,7 @@ pub fn use_navigate(
|
||||
Rc::clone(&router.inner).navigate_from_route(to, &options)
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Returns a signal that tells you whether you are currently navigating backwards.
|
||||
pub(crate) fn use_is_back_navigation(cx: Scope) -> ReadSignal<bool> {
|
||||
let router = use_router(cx);
|
||||
|
||||
@@ -15,7 +15,7 @@ serde_qs = "0.12"
|
||||
thiserror = "1"
|
||||
serde_json = "1"
|
||||
quote = "1"
|
||||
syn = { version = "1", features = ["full", "parsing", "extra-traits"] }
|
||||
syn = { version = "2", features = ["full", "parsing", "extra-traits"] }
|
||||
proc-macro2 = "1"
|
||||
ciborium = "0.2"
|
||||
xxhash-rust = { version = "0.8", features = ["const_xxh64"] }
|
||||
|
||||
@@ -11,7 +11,7 @@ description = "The default implementation of the server_fn macro without a conte
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = { version = "1", features = ["full"] }
|
||||
syn = { version = "2", features = ["full"] }
|
||||
server_fn_macro = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -11,7 +11,7 @@ readme = "../README.md"
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
quote = "1"
|
||||
syn = { version = "1", features = ["full", "parsing", "extra-traits"] }
|
||||
syn = { version = "2", features = ["full", "parsing", "extra-traits"] }
|
||||
proc-macro2 = "1"
|
||||
proc-macro-error = "1"
|
||||
xxhash-rust = { version = "0.8.6", features = ["const_xxh64"] }
|
||||
|
||||
@@ -75,9 +75,11 @@ pub fn server_macro_impl(
|
||||
struct_name,
|
||||
prefix,
|
||||
encoding,
|
||||
fn_path,
|
||||
..
|
||||
} = syn::parse2::<ServerFnName>(args)?;
|
||||
let prefix = prefix.unwrap_or_else(|| Literal::string(""));
|
||||
let fn_path = fn_path.unwrap_or_else(|| Literal::string(""));
|
||||
let encoding = quote!(#server_fn_path::#encoding);
|
||||
|
||||
let body = syn::parse::<ServerFnBody>(body.into())?;
|
||||
@@ -213,7 +215,11 @@ pub fn server_macro_impl(
|
||||
}
|
||||
|
||||
fn url() -> &'static str {
|
||||
if !#fn_path.is_empty(){
|
||||
#fn_path
|
||||
} else {
|
||||
#server_fn_path::const_format::concatcp!(#fn_name_as_str, #server_fn_path::xxhash_rust::const_xxh64::xxh64(concat!(env!(#key_env_var), ":", file!(), ":", line!(), ":", column!()).as_bytes(), 0))
|
||||
}
|
||||
}
|
||||
|
||||
fn encoding() -> #server_fn_path::Encoding {
|
||||
@@ -260,6 +266,8 @@ struct ServerFnName {
|
||||
prefix: Option<Literal>,
|
||||
_comma2: Option<Token![,]>,
|
||||
encoding: Path,
|
||||
_comma3: Option<Token![,]>,
|
||||
fn_path: Option<Literal>,
|
||||
}
|
||||
|
||||
impl Parse for ServerFnName {
|
||||
@@ -280,6 +288,8 @@ impl Parse for ServerFnName {
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|_| syn::parse_quote!(Encoding::Url));
|
||||
let _comma3 = input.parse()?;
|
||||
let fn_path = input.parse()?;
|
||||
|
||||
Ok(Self {
|
||||
struct_name,
|
||||
@@ -287,6 +297,8 @@ impl Parse for ServerFnName {
|
||||
prefix,
|
||||
_comma2,
|
||||
encoding,
|
||||
_comma3,
|
||||
fn_path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user