mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 14:52:35 -05:00
Compare commits
37 Commits
explicit-s
...
component-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4360a73392 | ||
|
|
50b0fe157a | ||
|
|
64a5d75ec4 | ||
|
|
baf3cc8712 | ||
|
|
23777ad67b | ||
|
|
08be1ba622 | ||
|
|
605398bcea | ||
|
|
aca2c131d4 | ||
|
|
9d950b97ff | ||
|
|
f6a299ae3c | ||
|
|
1ba602ec47 | ||
|
|
1f3dde5b4a | ||
|
|
a65cd67db3 | ||
|
|
bacd99260b | ||
|
|
2b726f1a88 | ||
|
|
5c45538e9f | ||
|
|
7f696a9ac4 | ||
|
|
bcd6e671f7 | ||
|
|
7a72f127de | ||
|
|
2ff5ec21c8 | ||
|
|
a1f94b609f | ||
|
|
da5034da33 | ||
|
|
0c509970b5 | ||
|
|
d894c4dcf9 | ||
|
|
dc15184781 | ||
|
|
3200068ab3 | ||
|
|
0a9da8d55e | ||
|
|
52ad546710 | ||
|
|
f88d2fa56a | ||
|
|
f63cb02277 | ||
|
|
4b363f9b33 | ||
|
|
7b376b6d3a | ||
|
|
8fbb4abc76 | ||
|
|
d0ff64daaa | ||
|
|
bb97234817 | ||
|
|
19698d86b6 | ||
|
|
21ef96806f |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ blob.rs
|
||||
Cargo.lock
|
||||
**/*.rs.bk
|
||||
.DS_Store
|
||||
.leptos.kdl
|
||||
|
||||
@@ -4,6 +4,7 @@ members = [
|
||||
"leptos",
|
||||
"leptos_dom",
|
||||
"leptos_core",
|
||||
"leptos_config",
|
||||
"leptos_macro",
|
||||
"leptos_reactive",
|
||||
"leptos_server",
|
||||
|
||||
@@ -5,7 +5,7 @@ fn leptos_ssr_bench(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
|
||||
b.iter(|| {
|
||||
_ = create_scope(|cx| {
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
#[component]
|
||||
fn Counter(cx: Scope, initial: i32) -> Element {
|
||||
let (value, set_value) = create_signal(cx, initial);
|
||||
|
||||
@@ -115,7 +115,7 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Element {
|
||||
set_mode(new_mode);
|
||||
});
|
||||
|
||||
let add_todo = move |ev: web_sys::Event| {
|
||||
let add_todo = move |ev: web_sys::KeyboardEvent| {
|
||||
let target = event_target::<HtmlInputElement>(&ev);
|
||||
ev.stop_propagation();
|
||||
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
|
||||
|
||||
@@ -11,7 +11,7 @@ fn leptos_todomvc_ssr(b: &mut Bencher) {
|
||||
use ::leptos::*;
|
||||
|
||||
b.iter(|| {
|
||||
_ = create_scope(|cx| {
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
let rendered = view! {
|
||||
cx,
|
||||
<TodoMVC todos=Todos::new(cx)/>
|
||||
@@ -63,7 +63,7 @@ fn leptos_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
use ::leptos::*;
|
||||
|
||||
b.iter(|| {
|
||||
_ = create_scope(|cx| {
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
let rendered = view! {
|
||||
cx,
|
||||
<TodoMVC todos=Todos::new_with_1000(cx)/>
|
||||
|
||||
@@ -9,6 +9,7 @@ cfg_if! {
|
||||
use actix_files::{Files};
|
||||
use actix_web::*;
|
||||
use crate::counters::*;
|
||||
use std::{net::SocketAddr, env};
|
||||
|
||||
#[get("/api/events")]
|
||||
async fn counter_events() -> impl Responder {
|
||||
@@ -29,17 +30,20 @@ cfg_if! {
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let addr = SocketAddr::from(([127,0,0,1],3000));
|
||||
crate::counters::register_server_functions();
|
||||
|
||||
HttpServer::new(|| {
|
||||
HttpServer::new(move || {
|
||||
let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/leptos_counter_isomorphic").reload_port(3001).socket_address(addr.clone()).environment(&env::var("RUST_ENV")).build();
|
||||
render_options.write_to_file();
|
||||
App::new()
|
||||
.service(Files::new("/pkg", "./pkg"))
|
||||
.service(counter_events)
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.route("/{tail:.*}", leptos_actix::render_app_to_stream("leptos_counter_isomorphic", |cx| view! { cx, <Counters/> }))
|
||||
.route("/{tail:.*}", leptos_actix::render_app_to_stream(render_options, |cx| view! { cx, <Counters/> }))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(("127.0.0.1", 8081))?
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
use leptos::*;
|
||||
|
||||
pub fn simple_counter(cx: Scope) -> web_sys::Element {
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
/// A simple counter component.
|
||||
///
|
||||
/// You can document each of the properties passed to a component using the format below.
|
||||
///
|
||||
/// # Props
|
||||
/// - **initial_value** [`i32`] - The value the counter should start at.
|
||||
/// - **step** [`i32`] - The change that should be applied on each step.
|
||||
#[component]
|
||||
pub fn SimpleCounter(cx: Scope, initial_value: i32, step: i32) -> web_sys::Element {
|
||||
let (value, set_value) = create_signal(cx, initial_value);
|
||||
|
||||
view! { cx,
|
||||
<div>
|
||||
<button on:click=move |_| set_value(0)>"Clear"</button>
|
||||
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
|
||||
<button on:click=move |_| set_value(initial_value)>"Clear"</button>
|
||||
<button on:click=move |_| set_value.update(|value| *value -= step)>"-1"</button>
|
||||
<span>"Value: " {move || value().to_string()} "!"</span>
|
||||
<button on:click=move |_| set_value.update(|value| *value += 1)>"+1"</button>
|
||||
<button on:click=move |_| set_value.update(|value| *value += step)>"+1"</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use counter::simple_counter;
|
||||
use counter::*;
|
||||
use leptos::*;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(simple_counter)
|
||||
mount_to_body(|cx| view! { cx, <SimpleCounter initial_value=0 step=1/> })
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@ use wasm_bindgen_test::*;
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
use leptos::*;
|
||||
use web_sys::HtmlElement;
|
||||
use counter::*;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn inc() {
|
||||
mount_to_body(counter::simple_counter);
|
||||
mount_to_body(|cx| view! { cx, <SimpleCounter initial_value=0 step=1/> });
|
||||
|
||||
let document = leptos::document();
|
||||
let div = document.query_selector("div").unwrap().unwrap();
|
||||
|
||||
@@ -12,11 +12,12 @@ if #[cfg(feature = "ssr")] {
|
||||
use http::StatusCode;
|
||||
use std::net::SocketAddr;
|
||||
use tower_http::services::ServeDir;
|
||||
use std::env;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use leptos_hackernews_axum::*;
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 8082));
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||
|
||||
log::debug!("serving at {addr}");
|
||||
|
||||
@@ -36,12 +37,14 @@ if #[cfg(feature = "ssr")] {
|
||||
)
|
||||
}
|
||||
|
||||
let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/leptos_hackernews_axum").socket_address(addr).reload_port(3001).environment(&env::var("RUST_ENV")).build();
|
||||
render_options.write_to_file();
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
// `GET /` goes to `root`
|
||||
.nest_service("/pkg", pkg_service)
|
||||
.nest_service("/static", static_service)
|
||||
.fallback(leptos_axum::render_app_to_stream("leptos_hackernews_axum", |cx| view! { cx, <App/> }));
|
||||
.fallback(leptos_axum::render_app_to_stream(render_options, |cx| view! { cx, <App/> }));
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
|
||||
@@ -14,11 +14,10 @@ console_log = "0.2"
|
||||
console_error_panic_hook = "0.1"
|
||||
futures = "0.3"
|
||||
cfg-if = "1"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
leptos = { version = "0.0.20", default-features = false, features = ["serde"] }
|
||||
leptos_meta = { version = "0.0", default-features = false }
|
||||
leptos_actix = { version = "0.0.2", default-features = false, optional = true }
|
||||
leptos_router = { version = "0.0", default-features = false }
|
||||
log = "0.4"
|
||||
simple_logger = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
@@ -35,11 +34,12 @@ hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:actix-files",
|
||||
"dep:actix-web",
|
||||
"dep:leptos_actix",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["actix-files", "actix-web"]
|
||||
denylist = ["actix-files", "actix-web", "leptos_actix"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
@@ -16,7 +16,7 @@ pub fn App(cx: Scope) -> Element {
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<Stylesheet href="/static/style.css"/>
|
||||
<Stylesheet href="/style.css"/>
|
||||
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
|
||||
<Router>
|
||||
<Nav />
|
||||
@@ -38,7 +38,7 @@ cfg_if! {
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn main() {
|
||||
pub fn hydrate() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::hydrate(body().unwrap(), move |cx| {
|
||||
|
||||
@@ -3,109 +3,39 @@ use leptos::*;
|
||||
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
// server-only stuff
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use actix_files::{Files, NamedFile};
|
||||
use actix_files::{Files};
|
||||
use actix_web::*;
|
||||
use futures::StreamExt;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use leptos_hackernews::*;
|
||||
use std::{net::SocketAddr, env};
|
||||
|
||||
#[get("/static/style.css")]
|
||||
#[get("/style.css")]
|
||||
async fn css() -> impl Responder {
|
||||
NamedFile::open_async("./style.css").await
|
||||
}
|
||||
|
||||
// match every path — our router will handle actual dispatch
|
||||
#[get("{tail:.*}")]
|
||||
async fn render_app(req: HttpRequest) -> impl Responder {
|
||||
let path = req.path();
|
||||
|
||||
let query = req.query_string();
|
||||
let path = if query.is_empty() {
|
||||
"http://leptos".to_string() + path
|
||||
} else {
|
||||
"http://leptos".to_string() + path + "?" + query
|
||||
};
|
||||
|
||||
let app = move |cx| {
|
||||
let integration = ServerIntegration { path: path.clone() };
|
||||
provide_context(cx, RouterIntegrationContext::new(integration));
|
||||
|
||||
view! { cx, <App/> }
|
||||
};
|
||||
|
||||
let head = r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<script type="module">import init, { main } from '/pkg/leptos_hackernews.js'; init().then(main);</script>"#;
|
||||
let tail = "</body></html>";
|
||||
|
||||
HttpResponse::Ok().content_type("text/html").streaming(
|
||||
futures::stream::once(async { head.to_string() })
|
||||
.chain(render_to_stream(move |cx| {
|
||||
let app = app(cx);
|
||||
let head = use_context::<MetaContext>(cx)
|
||||
.map(|meta| meta.dehydrate())
|
||||
.unwrap_or_default();
|
||||
format!("{head}</head><body>{app}")
|
||||
}))
|
||||
.chain(futures::stream::once(async { tail.to_string() }))
|
||||
.map(|html| Ok(web::Bytes::from(html)) as Result<web::Bytes>),
|
||||
)
|
||||
actix_files::NamedFile::open_async("./style.css").await
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let host = std::env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
|
||||
let port = std::env::var("PORT").unwrap_or_else(|_| "8080".to_string());
|
||||
log::debug!("serving at {host}:{port}");
|
||||
let addr = SocketAddr::from(([127,0,0,1],3000));
|
||||
|
||||
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
|
||||
|
||||
// uncomment these lines (and .bind_openssl() below) to enable HTTPS, which is sometimes
|
||||
// necessary for proper HTTP/2 streaming
|
||||
|
||||
// load TLS keys
|
||||
// to create a self-signed temporary cert for testing:
|
||||
// `openssl req -x509 -newkey rsa:4096 -nodes -keyout key.pem -out cert.pem -days 365 -subj '/CN=localhost'`
|
||||
// let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
|
||||
// builder
|
||||
// .set_private_key_file("key.pem", SslFiletype::PEM)
|
||||
// .unwrap();
|
||||
// builder.set_certificate_chain_file("cert.pem").unwrap();
|
||||
|
||||
HttpServer::new(|| {
|
||||
HttpServer::new(move || {
|
||||
let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/leptos_hackernews").reload_port(3001).socket_address(addr.clone()).environment(&env::var("RUST_ENV")).build();
|
||||
render_options.write_to_file();
|
||||
App::new()
|
||||
.service(Files::new("/pkg", "./pkg"))
|
||||
.service(css)
|
||||
.service(
|
||||
web::scope("/pkg")
|
||||
.service(Files::new("", "./pkg"))
|
||||
.wrap(middleware::Compress::default()),
|
||||
)
|
||||
.service(render_app)
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.route("/{tail:.*}", leptos_actix::render_app_to_stream(render_options, |cx| view! { cx, <App/> }))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(("127.0.0.1", 8080))?
|
||||
// replace .bind with .bind_openssl to use HTTPS
|
||||
//.bind_openssl(&format!("{}:{}", host, port), builder)?
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
// client-only stuff for Trunk
|
||||
else {
|
||||
use leptos_hackernews::*;
|
||||
|
||||
pub fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| {
|
||||
view! { cx, <App/> }
|
||||
});
|
||||
} else {
|
||||
fn main() {
|
||||
// no client-side main function
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ pub fn Nav(cx: Scope) -> Element {
|
||||
view! { cx,
|
||||
<header class="header">
|
||||
<nav class="inner">
|
||||
<A href="/">
|
||||
<A href="/" class="home".to_string()>
|
||||
<strong>"HN"</strong>
|
||||
</A>
|
||||
<A href="/new">
|
||||
|
||||
@@ -9,6 +9,7 @@ cfg_if! {
|
||||
use actix_files::{Files};
|
||||
use actix_web::*;
|
||||
use crate::todo::*;
|
||||
use std::{ net::SocketAddr,env };
|
||||
|
||||
#[get("/style.css")]
|
||||
async fn css() -> impl Responder {
|
||||
@@ -24,16 +25,19 @@ cfg_if! {
|
||||
.expect("could not run SQLx migrations");
|
||||
|
||||
crate::todo::register_server_functions();
|
||||
let addr = SocketAddr::from(([127,0,0,1],3000));
|
||||
|
||||
HttpServer::new(|| {
|
||||
HttpServer::new(move || {
|
||||
let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/todo_app_sqlite").reload_port(3001).socket_address(addr.clone()).environment(&env::var("RUST_ENV")).build();
|
||||
render_options.write_to_file();
|
||||
App::new()
|
||||
.service(Files::new("/pkg", "./pkg"))
|
||||
.service(css)
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.route("/{tail:.*}", leptos_actix::render_app_to_stream("todo_app_cbor", |cx| view! { cx, <TodoApp/> }))
|
||||
.route("/{tail:.*}", leptos_actix::render_app_to_stream(render_options, |cx| view! { cx, <TodoApp/> }))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(("127.0.0.1", 8081))?
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -14,10 +14,11 @@ if #[cfg(feature = "ssr")] {
|
||||
use todo_app_sqlite_axum::*;
|
||||
use http::StatusCode;
|
||||
use tower_http::services::ServeDir;
|
||||
use std::env;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 8082));
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||
log::debug!("serving at {addr}");
|
||||
|
||||
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
|
||||
@@ -45,17 +46,19 @@ if #[cfg(feature = "ssr")] {
|
||||
}
|
||||
|
||||
|
||||
let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/todo_app_sqlite_axum").socket_address(addr).reload_port(3001).environment(&env::var("RUST_ENV")).build();
|
||||
render_options.write_to_file();
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
.nest_service("/pkg", pkg_service)
|
||||
.nest_service("/static", static_service)
|
||||
.fallback(leptos_axum::render_app_to_stream("todo_app_sqlite_axum", |cx| view! { cx, <TodoApp/> }));
|
||||
.fallback(leptos_axum::render_app_to_stream(render_options.clone(), |cx| view! { cx, <TodoApp/> }));
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on {}", addr);
|
||||
axum::Server::bind(&addr)
|
||||
log!("listening on {}", &render_options.socket_address);
|
||||
axum::Server::bind(&render_options.socket_address)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -12,9 +12,9 @@ cfg_if! {
|
||||
}
|
||||
|
||||
pub fn register_server_functions() {
|
||||
GetTodos::register();
|
||||
AddTodo::register();
|
||||
DeleteTodo::register();
|
||||
_ = GetTodos::register();
|
||||
_ = AddTodo::register();
|
||||
_ = DeleteTodo::register();
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
|
||||
@@ -34,7 +34,7 @@ cfg_if! {
|
||||
}
|
||||
|
||||
#[server(GetTodos, "/api")]
|
||||
pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
|
||||
pub async fn get_todos(_cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
|
||||
// this is just an example of how to access server context injected in the handlers
|
||||
// http::Request doesn't implement Clone, so more work will be needed to do use_context() on this
|
||||
// let req = use_context::<http::Request<axum::body::BoxBody>>(cx)
|
||||
@@ -70,7 +70,7 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
{
|
||||
Ok(row) => Ok(()),
|
||||
Ok(_row) => Ok(()),
|
||||
Err(e) => Err(ServerFnError::ServerError(e.to_string())),
|
||||
}
|
||||
}
|
||||
@@ -167,7 +167,7 @@ pub fn Todos(cx: Scope) -> Element {
|
||||
<li>
|
||||
{todo.title}
|
||||
<ActionForm action=delete_todo.clone()>
|
||||
<input type="hidden" name="id" value={todo.id}/>
|
||||
<input type="hidden" name="id" value=todo.id/>
|
||||
<input type="submit" value="X"/>
|
||||
</ActionForm>
|
||||
</li>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
mod todo;
|
||||
@@ -9,6 +11,7 @@ cfg_if! {
|
||||
use actix_files::{Files};
|
||||
use actix_web::*;
|
||||
use crate::todo::*;
|
||||
use std::env;
|
||||
|
||||
#[get("/style.css")]
|
||||
async fn css() -> impl Responder {
|
||||
@@ -25,15 +28,19 @@ cfg_if! {
|
||||
|
||||
crate::todo::register_server_functions();
|
||||
|
||||
HttpServer::new(|| {
|
||||
let addr = SocketAddr::from(([127,0,0,1],3000));
|
||||
|
||||
HttpServer::new(move || {
|
||||
let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/todo_app_sqlite").reload_port(3001).socket_address(addr.clone()).environment(&env::var("RUST_ENV")).build();
|
||||
render_options.write_to_file();
|
||||
App::new()
|
||||
.service(Files::new("/pkg", "./pkg"))
|
||||
.service(css)
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.route("/{tail:.*}", leptos_actix::render_app_to_stream("todo_app_sqlite", |cx| view! { cx, <TodoApp/> }))
|
||||
.route("/{tail:.*}", leptos_actix::render_app_to_stream(render_options, |cx| view! { cx, <TodoApp/> }))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(("127.0.0.1", 8083))?
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -46,6 +46,9 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
|
||||
|
||||
let mut conn = db().await?;
|
||||
|
||||
// fake API delay
|
||||
std::thread::sleep(std::time::Duration::from_millis(350));
|
||||
|
||||
let mut todos = Vec::new();
|
||||
let mut rows = sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
|
||||
while let Some(row) = rows
|
||||
@@ -66,7 +69,7 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
let mut conn = db().await?;
|
||||
|
||||
// fake API delay
|
||||
std::thread::sleep(std::time::Duration::from_millis(1250));
|
||||
std::thread::sleep(std::time::Duration::from_millis(350));
|
||||
|
||||
sqlx::query("INSERT INTO todos (title, completed) VALUES ($1, false)")
|
||||
.bind(title)
|
||||
@@ -139,7 +142,7 @@ pub fn Todos(cx: Scope) -> Element {
|
||||
<input type="submit" value="Add"/>
|
||||
</MultiActionForm>
|
||||
<div>
|
||||
<Suspense fallback=view! {cx, <p>"Loading..."</p> }>
|
||||
<Transition fallback=view! {cx, <p>"Loading..."</p> }>
|
||||
{
|
||||
let delete_todo = delete_todo.clone();
|
||||
move || {
|
||||
@@ -208,7 +211,7 @@ pub fn Todos(cx: Scope) -> Element {
|
||||
}
|
||||
}
|
||||
}
|
||||
</Suspense>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ fn Tests(cx: Scope) -> Element {
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<div><SelfUpdatingEffect/></div>
|
||||
//<div><SelfUpdatingEffect/></div>
|
||||
<div><BlockOrders/></div>
|
||||
//<div><TemplateConsumer/></div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_actix"
|
||||
version = "0.0.1"
|
||||
version = "0.0.2"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -63,7 +63,10 @@ pub fn handle_server_fns() -> Route {
|
||||
runtime.dispose();
|
||||
|
||||
let mut res: HttpResponseBuilder;
|
||||
if accept_header.is_some() {
|
||||
if accept_header == Some("application/json")
|
||||
|| accept_header == Some("application/x-www-form-urlencoded")
|
||||
|| accept_header == Some("application/cbor")
|
||||
{
|
||||
res = HttpResponse::Ok()
|
||||
}
|
||||
// otherwise, it's probably a <form> submit or something: redirect back to the referrer
|
||||
@@ -87,7 +90,7 @@ pub fn handle_server_fns() -> Route {
|
||||
res.body(data)
|
||||
}
|
||||
Payload::Json(data) => {
|
||||
res.content_type("application/jsoon");
|
||||
res.content_type("application/json");
|
||||
res.body(data)
|
||||
}
|
||||
}
|
||||
@@ -116,6 +119,7 @@ pub fn handle_server_fns() -> Route {
|
||||
/// ```
|
||||
/// use actix_web::{HttpServer, App};
|
||||
/// use leptos::*;
|
||||
/// use std::{env,net::SocketAddr};
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyApp(cx: Scope) -> Element {
|
||||
@@ -125,23 +129,28 @@ pub fn handle_server_fns() -> Route {
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
/// #[actix_web::main]
|
||||
/// async fn main() -> std::io::Result<()> {
|
||||
/// HttpServer::new(|| {
|
||||
///
|
||||
/// let addr = SocketAddr::from(([127,0,0,1],3000));
|
||||
/// HttpServer::new(move || {
|
||||
/// let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/leptos_example").reload_port(3001).socket_address(addr.clone()).environment(&env::var("RUST_ENV")).build();
|
||||
/// render_options.write_to_file();
|
||||
/// App::new()
|
||||
/// // {tail:.*} passes the remainder of the URL as the route
|
||||
/// // the actual routing will be handled by `leptos_router`
|
||||
/// .route("/{tail:.*}", leptos_actix::render_app_to_stream("leptos_example", |cx| view! { cx, <MyApp/> }))
|
||||
/// .route("/{tail:.*}", leptos_actix::render_app_to_stream(render_options, |cx| view! { cx, <MyApp/> }))
|
||||
/// })
|
||||
/// .bind(("127.0.0.1", 8080))?
|
||||
/// .bind(&addr)?
|
||||
/// .run()
|
||||
/// .await
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn render_app_to_stream(
|
||||
client_pkg_name: &'static str,
|
||||
options: RenderOptions,
|
||||
app_fn: impl Fn(leptos::Scope) -> Element + Clone + 'static,
|
||||
) -> Route {
|
||||
web::get().to(move |req: HttpRequest| {
|
||||
let options = options.clone();
|
||||
let app_fn = app_fn.clone();
|
||||
async move {
|
||||
let path = req.path();
|
||||
@@ -165,12 +174,38 @@ pub fn render_app_to_stream(
|
||||
}
|
||||
};
|
||||
|
||||
let head = format!(r#"<!DOCTYPE html>
|
||||
<html>
|
||||
let pkg_path = &options.pkg_path;
|
||||
let socket_ip = &options.socket_address.ip().to_string();
|
||||
let reload_port = options.reload_port;
|
||||
|
||||
let leptos_autoreload = match options.environment {
|
||||
RustEnv::DEV => format!(
|
||||
r#"
|
||||
<script crossorigin="">(function () {{
|
||||
var ws = new WebSocket('ws://{socket_ip}:{reload_port}/autoreload');
|
||||
ws.onmessage = (ev) => {{
|
||||
console.log(`Reload message: `);
|
||||
if (ev.data === 'reload') window.location.reload();
|
||||
}};
|
||||
ws.onclose = () => console.warn('Autoreload stopped. Manual reload necessary.');
|
||||
}})()
|
||||
</script>
|
||||
"#
|
||||
),
|
||||
RustEnv::PROD => "".to_string(),
|
||||
};
|
||||
|
||||
let head = format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<script type="module">import init, {{ hydrate }} from '/pkg/{client_pkg_name}.js'; init().then(hydrate);</script>"#);
|
||||
<script type="module">import init, {{ hydrate }} from '{pkg_path}.js'; init().then(hydrate);</script>
|
||||
{leptos_autoreload}
|
||||
"#
|
||||
);
|
||||
|
||||
let tail = "</body></html>";
|
||||
|
||||
HttpResponse::Ok().content_type("text/html").streaming(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_axum"
|
||||
version = "0.0.2"
|
||||
version = "0.0.4"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -9,7 +9,9 @@ description = "Axum integrations for the Leptos web framework."
|
||||
|
||||
[dependencies]
|
||||
axum = "0.6"
|
||||
derive_builder = "0.12.0"
|
||||
futures = "0.3"
|
||||
kdl = "4.6.0"
|
||||
leptos = { path = "../../leptos", default-features = false, version = "0.0", features = [
|
||||
"ssr",
|
||||
] }
|
||||
|
||||
@@ -9,7 +9,6 @@ use leptos::*;
|
||||
use leptos_meta::MetaContext;
|
||||
use leptos_router::*;
|
||||
use std::{io, pin::Pin, sync::Arc};
|
||||
|
||||
/// 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].
|
||||
///
|
||||
@@ -76,7 +75,11 @@ pub async fn handle_server_fns(
|
||||
headers.get("Accept").and_then(|value| value.to_str().ok());
|
||||
let mut res = Response::builder();
|
||||
|
||||
if accept_header.is_some() {
|
||||
if accept_header == Some("application/json")
|
||||
|| accept_header
|
||||
== Some("application/x-www-form-urlencoded")
|
||||
|| accept_header == Some("application/cbor")
|
||||
{
|
||||
res = res.status(StatusCode::OK);
|
||||
}
|
||||
// otherwise, it's probably a <form> submit or something: redirect back to the referrer
|
||||
@@ -141,7 +144,7 @@ pub type PinnedHtmlStream = Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send>
|
||||
/// ```
|
||||
/// use axum::handler::Handler;
|
||||
/// use axum::Router;
|
||||
/// use std::net::SocketAddr;
|
||||
/// use std::{net::SocketAddr, env};
|
||||
/// use leptos::*;
|
||||
///
|
||||
/// #[component]
|
||||
@@ -153,10 +156,15 @@ pub type PinnedHtmlStream = Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send>
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let addr = SocketAddr::from(([127, 0, 0, 1], 8082));
|
||||
/// let render_options: RenderOptions = RenderOptions::builder()
|
||||
/// .pkg_path("/pkg/leptos_example")
|
||||
/// .socket_address(addr)
|
||||
/// .reload_port(3001)
|
||||
/// .environment(&env::var("RUST_ENV")).build();
|
||||
///
|
||||
/// // build our application with a route
|
||||
/// let app = Router::new()
|
||||
/// .fallback(leptos_axum::render_app_to_stream("leptos_example", |cx| view! { cx, <MyApp/> }));
|
||||
/// .fallback(leptos_axum::render_app_to_stream(render_options, |cx| view! { cx, <MyApp/> }));
|
||||
///
|
||||
/// // run our app with hyper
|
||||
/// // `axum::Server` is a re-export of `hyper::Server`
|
||||
@@ -167,8 +175,9 @@ pub type PinnedHtmlStream = Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send>
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
pub fn render_app_to_stream(
|
||||
client_pkg_name: &'static str,
|
||||
options: RenderOptions,
|
||||
app_fn: impl Fn(leptos::Scope) -> Element + Clone + Send + 'static,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
@@ -178,6 +187,7 @@ pub fn render_app_to_stream(
|
||||
+ 'static {
|
||||
move |req: Request<Body>| {
|
||||
Box::pin({
|
||||
let options = options.clone();
|
||||
let app_fn = app_fn.clone();
|
||||
async move {
|
||||
// Need to get the path and query string of the Request
|
||||
@@ -191,13 +201,36 @@ pub fn render_app_to_stream(
|
||||
full_path = "http://leptos".to_string() + &path.to_string()
|
||||
}
|
||||
|
||||
let pkg_path = &options.pkg_path;
|
||||
let socket_ip = &options.socket_address.ip().to_string();
|
||||
let reload_port = options.reload_port;
|
||||
|
||||
let leptos_autoreload = match options.environment {
|
||||
RustEnv::DEV => format!(
|
||||
r#"
|
||||
<script crossorigin="">(function () {{
|
||||
var ws = new WebSocket('ws://{socket_ip}:{reload_port}/autoreload');
|
||||
ws.onmessage = (ev) => {{
|
||||
console.log(`Reload message: `);
|
||||
if (ev.data === 'reload') window.location.reload();
|
||||
}};
|
||||
ws.onclose = () => console.warn('Autoreload stopped. Manual reload necessary.');
|
||||
}})()
|
||||
</script>
|
||||
"#
|
||||
),
|
||||
RustEnv::PROD => "".to_string(),
|
||||
};
|
||||
|
||||
let head = format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<script type="module">import init, {{ hydrate }} from '/pkg/{client_pkg_name}.js'; init().then(hydrate);</script>"#
|
||||
<script type="module">import init, {{ hydrate }} from '{pkg_path}.js'; init().then(hydrate);</script>
|
||||
{leptos_autoreload}
|
||||
"#
|
||||
);
|
||||
let tail = "</body></html>";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos"
|
||||
version = "0.0.19"
|
||||
version = "0.0.20"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -9,11 +9,12 @@ description = "Leptos is a full-stack, isomorphic Rust web framework leveraging
|
||||
readme = "../README.md"
|
||||
|
||||
[dependencies]
|
||||
leptos_core = { path = "../leptos_core", default-features = false, version = "0.0.19" }
|
||||
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.19" }
|
||||
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.19" }
|
||||
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.19" }
|
||||
leptos_server = { path = "../leptos_server", default-features = false, version = "0.0.19" }
|
||||
leptos_core = { path = "../leptos_core", default-features = false, version = "0.0.20" }
|
||||
leptos_config = { path = "../leptos_config", default-features = false, version = "0.0.20" }
|
||||
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.20" }
|
||||
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.20" }
|
||||
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.20" }
|
||||
leptos_server = { path = "../leptos_server", default-features = false, version = "0.0.20" }
|
||||
|
||||
[build-dependencies]
|
||||
rustc_version = "0.4"
|
||||
@@ -21,32 +22,32 @@ rustc_version = "0.4"
|
||||
[features]
|
||||
default = ["csr", "serde", "interning"]
|
||||
csr = [
|
||||
"leptos_core/csr",
|
||||
"leptos_dom/csr",
|
||||
"leptos_macro/csr",
|
||||
"leptos_reactive/csr",
|
||||
"leptos_server/csr",
|
||||
"leptos_core/csr",
|
||||
"leptos_dom/csr",
|
||||
"leptos_macro/csr",
|
||||
"leptos_reactive/csr",
|
||||
"leptos_server/csr",
|
||||
]
|
||||
hydrate = [
|
||||
"leptos_core/hydrate",
|
||||
"leptos_dom/hydrate",
|
||||
"leptos_macro/hydrate",
|
||||
"leptos_reactive/hydrate",
|
||||
"leptos_server/hydrate",
|
||||
"leptos_core/hydrate",
|
||||
"leptos_dom/hydrate",
|
||||
"leptos_macro/hydrate",
|
||||
"leptos_reactive/hydrate",
|
||||
"leptos_server/hydrate",
|
||||
]
|
||||
ssr = [
|
||||
"leptos_core/ssr",
|
||||
"leptos_dom/ssr",
|
||||
"leptos_macro/ssr",
|
||||
"leptos_reactive/ssr",
|
||||
"leptos_server/ssr",
|
||||
"leptos_core/ssr",
|
||||
"leptos_dom/ssr",
|
||||
"leptos_macro/ssr",
|
||||
"leptos_reactive/ssr",
|
||||
"leptos_server/ssr",
|
||||
]
|
||||
stable = [
|
||||
"leptos_core/stable",
|
||||
"leptos_dom/stable",
|
||||
"leptos_macro/stable",
|
||||
"leptos_reactive/stable",
|
||||
"leptos_server/stable",
|
||||
"leptos_core/stable",
|
||||
"leptos_dom/stable",
|
||||
"leptos_macro/stable",
|
||||
"leptos_reactive/stable",
|
||||
"leptos_server/stable",
|
||||
]
|
||||
serde = ["leptos_reactive/serde"]
|
||||
serde-lite = ["leptos_reactive/serde-lite"]
|
||||
|
||||
@@ -141,6 +141,7 @@
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
pub use leptos_config::*;
|
||||
pub use leptos_core::*;
|
||||
pub use leptos_dom;
|
||||
pub use leptos_dom::wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||
|
||||
11
leptos_config/Cargo.toml
Normal file
11
leptos_config/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "leptos_config"
|
||||
version = "0.0.20"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/gbj/leptos"
|
||||
description = "Configuraiton for the Leptos web framework."
|
||||
|
||||
[dependencies]
|
||||
typed-builder = "0.11.0"
|
||||
110
leptos_config/src/lib.rs
Normal file
110
leptos_config/src/lib.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use std::{env::VarError, net::SocketAddr, str::FromStr};
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
/// This struct serves as a convenient place to store details used for rendering.
|
||||
/// It's serialized into a file in the root called `.leptos.kdl` for cargo-leptos
|
||||
/// to watch. It's also used in our actix and axum integrations to generate the
|
||||
/// correct path for WASM, JS, and Websockets. Its goal is to be the single source
|
||||
/// of truth for render options
|
||||
#[derive(TypedBuilder, Clone)]
|
||||
pub struct RenderOptions {
|
||||
/// The path and name of the WASM and JS files generated by wasm-bindgen
|
||||
/// For example, `/pkg/app` might be a valid input if your crate name was `app`.
|
||||
#[builder(setter(into))]
|
||||
pub pkg_path: String,
|
||||
/// Used to control whether the Websocket code for code watching is included.
|
||||
/// I recommend passing in the result of `env::var("RUST_ENV")`
|
||||
#[builder(setter(into), default)]
|
||||
pub environment: RustEnv,
|
||||
/// Provides a way to control the address leptos is served from.
|
||||
/// Using an env variable here would allow you to run the same code in dev and prod
|
||||
/// Defaults to `127.0.0.1:3000`
|
||||
#[builder(setter(into), default=SocketAddr::from(([127,0,0,1], 3000)))]
|
||||
pub socket_address: SocketAddr,
|
||||
/// The port the Websocket watcher listens on. Should match the `reload_port` in cargo-leptos(if using).
|
||||
/// Defaults to `3001`
|
||||
#[builder(default = 3001)]
|
||||
pub reload_port: u32,
|
||||
}
|
||||
|
||||
impl RenderOptions {
|
||||
/// Creates a hidden file at ./.leptos_toml so cargo-leptos can monitor settings. We do not read from this file
|
||||
/// only write to it, you'll want to change the settings in your main function when you create RenderOptions
|
||||
pub fn write_to_file(&self) {
|
||||
use std::fs;
|
||||
let options = format!(
|
||||
r#"// This file is auto-generated. Changing it will have no effect on leptos. Change these by changing RenderOptions and rerunning
|
||||
RenderOptions {{
|
||||
pkg_path "{}"
|
||||
environment "{:?}"
|
||||
socket_address "{:?}"
|
||||
reload_port {:?}
|
||||
}}
|
||||
"#,
|
||||
self.pkg_path, self.environment, self.socket_address, self.reload_port
|
||||
);
|
||||
fs::write("./.leptos.kdl", options).expect("Unable to write file");
|
||||
}
|
||||
}
|
||||
/// An enum that can be used to define the environment Leptos is running in. Can be passed to RenderOptions.
|
||||
/// Setting this to the PROD variant will not include the websockets code for cargo-leptos' watch.
|
||||
/// Defaults to PROD
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RustEnv {
|
||||
PROD,
|
||||
DEV,
|
||||
}
|
||||
|
||||
impl Default for RustEnv {
|
||||
fn default() -> Self {
|
||||
Self::PROD
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for RustEnv {
|
||||
type Err = ();
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
let sanitized = input.to_lowercase();
|
||||
match sanitized.as_ref() {
|
||||
"dev" => Ok(Self::DEV),
|
||||
"development" => Ok(Self::DEV),
|
||||
"prod" => Ok(Self::PROD),
|
||||
"production" => Ok(Self::PROD),
|
||||
_ => Ok(Self::PROD),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for RustEnv {
|
||||
fn from(str: &str) -> Self {
|
||||
let sanitized = str.to_lowercase();
|
||||
match sanitized.as_str() {
|
||||
"dev" => Self::DEV,
|
||||
"development" => Self::DEV,
|
||||
"prod" => Self::PROD,
|
||||
"production" => Self::PROD,
|
||||
_ => {
|
||||
panic!("Environment var is not recognized. Maybe try `dev` or `prod`")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<&Result<String, VarError>> for RustEnv {
|
||||
fn from(input: &Result<String, VarError>) -> Self {
|
||||
match input {
|
||||
Ok(str) => {
|
||||
let sanitized = str.to_lowercase();
|
||||
match sanitized.as_ref() {
|
||||
"dev" => Self::DEV,
|
||||
"development" => Self::DEV,
|
||||
"prod" => Self::PROD,
|
||||
"production" => Self::PROD,
|
||||
_ => {
|
||||
panic!("Environment var is not recognized. Maybe try `dev` or `prod`")
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => Self::PROD,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_core"
|
||||
version = "0.0.19"
|
||||
version = "0.0.20"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -8,9 +8,9 @@ repository = "https://github.com/gbj/leptos"
|
||||
description = "Core functionality for the Leptos web framework."
|
||||
|
||||
[dependencies]
|
||||
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.19" }
|
||||
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.19" }
|
||||
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.19" }
|
||||
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.20" }
|
||||
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.20" }
|
||||
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.20" }
|
||||
log = "0.4"
|
||||
typed-builder = "0.11"
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
use leptos_dom::Element;
|
||||
use leptos_macro::*;
|
||||
use leptos_reactive::{Memo, Scope};
|
||||
use std::fmt::Debug;
|
||||
use std::hash::Hash;
|
||||
|
||||
use crate as leptos;
|
||||
use crate::map::map_keyed;
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
/// Properties for the [For](crate::For) component, a keyed list.
|
||||
#[derive(Props)]
|
||||
#[derive(TypedBuilder)]
|
||||
pub struct ForProps<E, T, G, I, K>
|
||||
where
|
||||
E: Fn() -> Vec<T>,
|
||||
|
||||
@@ -12,6 +12,7 @@ pub use for_component::*;
|
||||
pub use map::*;
|
||||
pub use suspense::*;
|
||||
pub use transition::*;
|
||||
pub use typed_builder;
|
||||
|
||||
/// Describes the properties of a component. This is typically generated by the `Prop` derive macro
|
||||
/// as part of the `#[component]` macro.
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use crate as leptos;
|
||||
use leptos_dom::{Child, IntoChild};
|
||||
use leptos_macro::Props;
|
||||
use leptos_reactive::{provide_context, Scope, SuspenseContext};
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
/// Props for the [Suspense](crate::Suspense) component, which shows a fallback
|
||||
/// while [Resource](leptos_reactive::Resource)s are being read.
|
||||
#[derive(Props)]
|
||||
#[derive(TypedBuilder)]
|
||||
pub struct SuspenseProps<F, E, G>
|
||||
where
|
||||
F: IntoChild + Clone,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_dom"
|
||||
version = "0.0.19"
|
||||
version = "0.0.20"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -12,7 +12,7 @@ cfg-if = "1"
|
||||
futures = "0.3"
|
||||
html-escape = "0.2"
|
||||
js-sys = "0.3"
|
||||
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.19" }
|
||||
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.20" }
|
||||
serde_json = "1"
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4.31"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
use wasm_bindgen::convert::FromWasmAbi;
|
||||
use wasm_bindgen::{prelude::Closure, JsCast, JsValue, UnwrapThrowExt};
|
||||
|
||||
@@ -254,30 +255,58 @@ pub fn set_interval(
|
||||
Ok(IntervalHandle(handle))
|
||||
}
|
||||
|
||||
/// Adds an event listener to the target DOM element using implicit event delegation.
|
||||
pub fn add_event_listener<E>(
|
||||
target: &web_sys::Element,
|
||||
event_name: &'static str,
|
||||
cb: impl FnMut(E) + 'static,
|
||||
) where
|
||||
E: FromWasmAbi + 'static,
|
||||
{
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
|
||||
let key = event_delegation::event_delegation_key(event_name);
|
||||
_ = js_sys::Reflect::set(target, &JsValue::from_str(&key), &cb);
|
||||
event_delegation::add_event_listener(event_name);
|
||||
}
|
||||
cfg_if! {
|
||||
if #[cfg(not(feature = "stable"))] {
|
||||
/// Adds an event listener to the target DOM element using implicit event delegation.
|
||||
pub fn add_event_listener<E>(
|
||||
target: &web_sys::Element,
|
||||
event_name: &'static str,
|
||||
cb: impl FnMut(E) + 'static,
|
||||
) where
|
||||
E: FromWasmAbi + 'static,
|
||||
{
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
|
||||
let key = event_delegation::event_delegation_key(event_name);
|
||||
_ = js_sys::Reflect::set(target, &JsValue::from_str(&key), &cb);
|
||||
event_delegation::add_event_listener(event_name);
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn add_event_listener_undelegated<E>(
|
||||
target: &web_sys::Element,
|
||||
event_name: &'static str,
|
||||
cb: impl FnMut(E) + 'static,
|
||||
) where
|
||||
E: FromWasmAbi + 'static,
|
||||
{
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
|
||||
_ = target.add_event_listener_with_callback(event_name, cb.unchecked_ref());
|
||||
#[doc(hidden)]
|
||||
pub fn add_event_listener_undelegated<E>(
|
||||
target: &web_sys::Element,
|
||||
event_name: &'static str,
|
||||
cb: impl FnMut(E) + 'static,
|
||||
) where
|
||||
E: FromWasmAbi + 'static,
|
||||
{
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
|
||||
_ = target.add_event_listener_with_callback(event_name, cb.unchecked_ref());
|
||||
}
|
||||
} else {
|
||||
/// Adds an event listener to the target DOM element using implicit event delegation.
|
||||
pub fn add_event_listener(
|
||||
target: &web_sys::Element,
|
||||
event_name: &'static str,
|
||||
cb: impl FnMut(web_sys::Event) + 'static,
|
||||
)
|
||||
{
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(web_sys::Event)>).into_js_value();
|
||||
let key = event_delegation::event_delegation_key(event_name);
|
||||
_ = js_sys::Reflect::set(target, &JsValue::from_str(&key), &cb);
|
||||
event_delegation::add_event_listener(event_name);
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn add_event_listener_undelegated(
|
||||
target: &web_sys::Element,
|
||||
event_name: &'static str,
|
||||
cb: impl FnMut(web_sys::Event) + 'static,
|
||||
)
|
||||
{
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(web_sys::Event)>).into_js_value();
|
||||
_ = target.add_event_listener_with_callback(event_name, cb.unchecked_ref());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_macro"
|
||||
version = "0.0.19"
|
||||
version = "0.0.20"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -12,15 +12,16 @@ proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
cfg-if = "1"
|
||||
itertools = "0.10"
|
||||
proc-macro-error = "1"
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
syn = { version = "1", features = ["full", "parsing", "extra-traits"] }
|
||||
syn-rsx = "0.9"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
leptos_dom = { path = "../leptos_dom", version = "0.0.19" }
|
||||
leptos_reactive = { path = "../leptos_reactive", version = "0.0.19" }
|
||||
leptos_server = { path = "../leptos_server", version = "0.0.19" }
|
||||
leptos_dom = { path = "../leptos_dom", version = "0.0.20" }
|
||||
leptos_reactive = { path = "../leptos_reactive", version = "0.0.20" }
|
||||
leptos_server = { path = "../leptos_server", version = "0.0.20" }
|
||||
lazy_static = "1.4"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
// Credit to Dioxus: https://github.com/DioxusLabs/dioxus/blob/master/packages/core-macro/src/inlineprops.rs
|
||||
// Based in large part on Dioxus: https://github.com/DioxusLabs/dioxus/blob/master/packages/core-macro/src/inlineprops.rs
|
||||
|
||||
use proc_macro2::{Span, TokenStream as TokenStream2};
|
||||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
#![allow(unstable_name_collisions)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use proc_macro2::{Span, TokenStream as TokenStream2, TokenTree};
|
||||
use quote::{quote, ToTokens, TokenStreamExt,};
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
punctuated::Punctuated,
|
||||
*,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
|
||||
pub struct InlinePropsBody {
|
||||
pub attrs: Vec<Attribute>,
|
||||
@@ -21,6 +26,7 @@ pub struct InlinePropsBody {
|
||||
pub output: ReturnType,
|
||||
pub where_clause: Option<WhereClause>,
|
||||
pub block: Box<Block>,
|
||||
pub doc_comment: String
|
||||
}
|
||||
|
||||
/// The custom rusty variant of parsing rsx!
|
||||
@@ -57,6 +63,24 @@ impl Parse for InlinePropsBody {
|
||||
|
||||
let block = input.parse()?;
|
||||
|
||||
let doc_comment = attrs.iter().filter_map(|attr| if attr.path.segments[0].ident == "doc" {
|
||||
|
||||
Some(attr.clone().tokens.into_iter().filter_map(|token| if let TokenTree::Literal(_) = token {
|
||||
// remove quotes
|
||||
let chars = token.to_string();
|
||||
let mut chars = chars.chars();
|
||||
chars.next();
|
||||
chars.next_back();
|
||||
Some(chars.as_str().to_string())
|
||||
} else {
|
||||
None
|
||||
}).collect::<String>())
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.intersperse_with(|| "\n".to_string())
|
||||
.collect();
|
||||
|
||||
Ok(Self {
|
||||
vis,
|
||||
fn_token,
|
||||
@@ -69,6 +93,7 @@ impl Parse for InlinePropsBody {
|
||||
block,
|
||||
cx_token,
|
||||
attrs,
|
||||
doc_comment
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -86,29 +111,86 @@ impl ToTokens for InlinePropsBody {
|
||||
block,
|
||||
cx_token,
|
||||
attrs,
|
||||
doc_comment,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let field_docs: HashMap<String, String> = {
|
||||
let mut map = HashMap::new();
|
||||
let mut pieces = doc_comment.split("# Props");
|
||||
pieces.next();
|
||||
let rest = pieces.next().unwrap_or_default();
|
||||
let mut current_field_name = String::new();
|
||||
let mut current_field_value = String::new();
|
||||
for line in rest.split('\n') {
|
||||
if let Some(line) = line.strip_prefix(" - ") {
|
||||
let mut pieces = line.split("**");
|
||||
pieces.next();
|
||||
let field_name = pieces.next();
|
||||
let field_value = pieces.next().unwrap_or_default();
|
||||
let field_value = if let Some((_ty, desc)) = field_value.split_once('-') {
|
||||
desc
|
||||
} else {
|
||||
field_value
|
||||
};
|
||||
if let Some(field_name) = field_name {
|
||||
if !current_field_name.is_empty() {
|
||||
map.insert(current_field_name.clone(), current_field_value.clone());
|
||||
}
|
||||
current_field_name = field_name.to_string();
|
||||
current_field_value = String::new();
|
||||
current_field_value.push_str(field_value);
|
||||
} else {
|
||||
current_field_value.push_str(field_value);
|
||||
}
|
||||
} else {
|
||||
current_field_value.push_str(line);
|
||||
}
|
||||
}
|
||||
if !current_field_name.is_empty() {
|
||||
map.insert(current_field_name, current_field_value.clone());
|
||||
}
|
||||
|
||||
map
|
||||
};
|
||||
|
||||
let fields = inputs.iter().map(|f| {
|
||||
let typed_arg = match f {
|
||||
FnArg::Receiver(_) => todo!(),
|
||||
FnArg::Typed(t) => t,
|
||||
};
|
||||
let comment = if let Pat::Ident(ident) = &*typed_arg.pat {
|
||||
field_docs.get(&ident.ident.to_string()).cloned()
|
||||
} else {
|
||||
None
|
||||
}.unwrap_or_default();
|
||||
let comment_macro = quote! {
|
||||
#[doc = #comment]
|
||||
};
|
||||
if let Type::Path(pat) = &*typed_arg.ty {
|
||||
if pat.path.segments[0].ident == "Option" {
|
||||
quote! {
|
||||
#[builder(default, setter(strip_option))]
|
||||
#vis #f
|
||||
#comment_macro
|
||||
#[builder(default, setter(strip_option, doc = #comment))]
|
||||
pub #f
|
||||
}
|
||||
} else {
|
||||
quote! { #vis #f }
|
||||
quote! {
|
||||
#comment_macro
|
||||
#[builder(setter(doc = #comment))]
|
||||
pub #f
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! { #vis #f }
|
||||
quote! {
|
||||
#comment
|
||||
#vis #f
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let struct_name = Ident::new(&format!("{}Props", ident), Span::call_site());
|
||||
let prop_struct_comments = format!("Props for the [`{ident}`] component.");
|
||||
|
||||
let field_names = inputs.iter().filter_map(|f| match f {
|
||||
FnArg::Receiver(_) => todo!(),
|
||||
@@ -122,7 +204,10 @@ impl ToTokens for InlinePropsBody {
|
||||
};
|
||||
|
||||
//let modifiers = if first_lifetime.is_some() {
|
||||
let modifiers = quote! { #[derive(Props)] };
|
||||
let modifiers = quote! {
|
||||
#[derive(leptos::typed_builder::TypedBuilder)]
|
||||
#[builder(doc)]
|
||||
};
|
||||
/* } else {
|
||||
quote! { #[derive(Props, PartialEq, Eq)] }
|
||||
}; */
|
||||
@@ -148,18 +233,14 @@ impl ToTokens for InlinePropsBody {
|
||||
quote! { <#struct_generics> },
|
||||
)
|
||||
} else {
|
||||
let lifetime: LifetimeDef = parse_quote! { 'a };
|
||||
let fn_generics = generics.clone();
|
||||
|
||||
let mut fn_generics = generics.clone();
|
||||
fn_generics
|
||||
.params
|
||||
.insert(0, GenericParam::Lifetime(lifetime.clone()));
|
||||
|
||||
(quote! { #lifetime, }, fn_generics, quote! { #generics })
|
||||
(quote! { }, fn_generics, quote! { #generics })
|
||||
};
|
||||
|
||||
out_tokens.append_all(quote! {
|
||||
#modifiers
|
||||
#[doc = #prop_struct_comments]
|
||||
#[allow(non_camel_case_types)]
|
||||
#vis struct #struct_name #struct_generics
|
||||
#where_clause
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
use proc_macro::{TokenStream, TokenTree};
|
||||
use quote::ToTokens;
|
||||
use server::server_macro_impl;
|
||||
use syn::{parse_macro_input, DeriveInput};
|
||||
use syn_rsx::{parse, NodeElement};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
@@ -34,7 +33,6 @@ mod params;
|
||||
mod view;
|
||||
use view::render_view;
|
||||
mod component;
|
||||
mod props;
|
||||
mod server;
|
||||
|
||||
/// The `view` macro uses RSX (like JSX, but Rust!) It follows most of the
|
||||
@@ -408,15 +406,6 @@ pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
}
|
||||
}
|
||||
|
||||
#[proc_macro_derive(Props, attributes(builder))]
|
||||
pub fn derive_prop(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as DeriveInput);
|
||||
|
||||
props::impl_derive_prop(&input)
|
||||
.unwrap_or_else(|err| err.to_compile_error())
|
||||
.into()
|
||||
}
|
||||
|
||||
// Derive Params trait for routing
|
||||
#[proc_macro_derive(Params, attributes(params))]
|
||||
pub fn params_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -564,14 +564,28 @@ fn attr_to_tokens(
|
||||
let event_type = event_type.parse::<TokenStream>().expect("couldn't parse event name");
|
||||
|
||||
if mode != Mode::Ssr {
|
||||
if NON_BUBBLING_EVENTS.contains(&name.as_str()) {
|
||||
expressions.push(quote_spanned! {
|
||||
span => ::leptos::add_event_listener_undelegated::<web_sys::#event_type>(#el_id.unchecked_ref(), #name, #handler);
|
||||
});
|
||||
} else {
|
||||
expressions.push(quote_spanned! {
|
||||
span => ::leptos::add_event_listener::<web_sys::#event_type>(#el_id.unchecked_ref(), #name, #handler);
|
||||
});
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "stable")] {
|
||||
if NON_BUBBLING_EVENTS.contains(&name.as_str()) {
|
||||
expressions.push(quote_spanned! {
|
||||
span => ::leptos::add_event_listener_undelegated(#el_id.unchecked_ref(), #name, #handler);
|
||||
});
|
||||
} else {
|
||||
expressions.push(quote_spanned! {
|
||||
span => ::leptos::add_event_listener(#el_id.unchecked_ref(), #name, #handler);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if NON_BUBBLING_EVENTS.contains(&name.as_str()) {
|
||||
expressions.push(quote_spanned! {
|
||||
span => ::leptos::add_event_listener_undelegated::<web_sys::#event_type>(#el_id.unchecked_ref(), #name, #handler);
|
||||
});
|
||||
} else {
|
||||
expressions.push(quote_spanned! {
|
||||
span => ::leptos::add_event_listener::<web_sys::#event_type>(#el_id.unchecked_ref(), #name, #handler);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -865,9 +879,12 @@ fn block_to_tokens(
|
||||
//next_sib = Some(el.clone());
|
||||
|
||||
template.push_str("<!#><!/>");
|
||||
let end = Ident::new(&format!("{co}_end"), span);
|
||||
|
||||
navigations.push(quote! {
|
||||
#location;
|
||||
let (#el, #co) = #cx.get_next_marker(&#name);
|
||||
let #end = #co.last().cloned().unwrap_or_else(|| #el.next_sibling().unwrap_throw());
|
||||
//log::debug!("get_next_marker => {}", #el.node_name());
|
||||
});
|
||||
|
||||
@@ -881,6 +898,8 @@ fn block_to_tokens(
|
||||
);
|
||||
});
|
||||
|
||||
return PrevSibChange::Sib(end);
|
||||
|
||||
//current = Some(el);
|
||||
}
|
||||
// in SSR, it needs to insert the value, wrapped in comments
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_reactive"
|
||||
version = "0.0.19"
|
||||
version = "0.0.20"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_server"
|
||||
version = "0.0.19"
|
||||
version = "0.0.20"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -8,8 +8,8 @@ repository = "https://github.com/gbj/leptos"
|
||||
description = "RPC for the Leptos web framework."
|
||||
|
||||
[dependencies]
|
||||
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.19" }
|
||||
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.19" }
|
||||
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.20" }
|
||||
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.20" }
|
||||
form_urlencoded = "1"
|
||||
gloo-net = "0.2"
|
||||
lazy_static = "1"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.0.4"
|
||||
version = "0.0.5"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -21,3 +21,7 @@ default = ["csr"]
|
||||
csr = ["leptos/csr"]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = ["leptos/ssr"]
|
||||
stable = ["leptos/stable"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["stable"]
|
||||
|
||||
@@ -70,16 +70,16 @@ impl MetaTagsContext {
|
||||
pub struct MetaProps {
|
||||
/// The [`charset`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-charset) attribute.
|
||||
#[builder(default, setter(strip_option, into))]
|
||||
charset: Option<TextProp>,
|
||||
pub charset: Option<TextProp>,
|
||||
/// The [`name`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-name) attribute.
|
||||
#[builder(default, setter(strip_option, into))]
|
||||
name: Option<TextProp>,
|
||||
pub name: Option<TextProp>,
|
||||
/// The [`http-equiv`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-http-equiv) attribute.
|
||||
#[builder(default, setter(strip_option, into))]
|
||||
http_equiv: Option<TextProp>,
|
||||
pub http_equiv: Option<TextProp>,
|
||||
/// The [`content`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-content) attribute.
|
||||
#[builder(default, setter(strip_option, into))]
|
||||
content: Option<TextProp>,
|
||||
pub content: Option<TextProp>,
|
||||
}
|
||||
|
||||
/// Injects an [HTMLMetaElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMetaElement) into the document
|
||||
|
||||
@@ -26,7 +26,7 @@ impl StylesheetContext {
|
||||
pub struct StylesheetProps {
|
||||
/// The URL at which the stylesheet can be located.
|
||||
#[builder(setter(into))]
|
||||
href: String,
|
||||
pub href: String,
|
||||
}
|
||||
|
||||
/// Injects an [HTMLLinkElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document
|
||||
|
||||
@@ -50,10 +50,10 @@ where
|
||||
pub struct TitleProps {
|
||||
/// A function that will be applied to any text value before it’s set as the title.
|
||||
#[builder(default, setter(strip_option, into))]
|
||||
formatter: Option<Formatter>,
|
||||
// Sets the the current `document.title`.
|
||||
pub formatter: Option<Formatter>,
|
||||
/// Sets the the current `document.title`.
|
||||
#[builder(default, setter(strip_option, into))]
|
||||
text: Option<TextProp>,
|
||||
pub text: Option<TextProp>,
|
||||
}
|
||||
|
||||
/// A component to set the document’s title by creating an [HTMLTitleElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTitleElement).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.0.5"
|
||||
version = "0.0.6"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -21,7 +21,6 @@ bincode = "1"
|
||||
url = { version = "2", optional = true }
|
||||
urlencoding = "2"
|
||||
thiserror = "1"
|
||||
typed-builder = "0.10"
|
||||
serde_urlencoded = "0.7"
|
||||
serde = "1"
|
||||
js-sys = { version = "0.3" }
|
||||
@@ -58,7 +57,8 @@ default = ["csr"]
|
||||
csr = ["leptos/csr"]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = ["leptos/ssr", "dep:url", "dep:regex"]
|
||||
stable = ["leptos/stable"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
# No need to test optional dependencies as they are enabled by the ssr feature
|
||||
denylist = ["url", "regex"]
|
||||
denylist = ["url", "regex", "stable"]
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::{use_navigate, use_resolved_path, ToHref};
|
||||
use crate::{use_navigate, use_resolved_path, TextProp};
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos::typed_builder::*;
|
||||
use std::{error::Error, rc::Rc};
|
||||
use typed_builder::TypedBuilder;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
|
||||
@@ -11,7 +12,7 @@ use wasm_bindgen_futures::JsFuture;
|
||||
#[derive(TypedBuilder)]
|
||||
pub struct FormProps<A>
|
||||
where
|
||||
A: ToHref + 'static,
|
||||
A: TextProp + 'static,
|
||||
{
|
||||
/// [`method`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-method)
|
||||
/// is the HTTP method to submit the form with (`get` or `post`).
|
||||
@@ -48,7 +49,7 @@ where
|
||||
#[allow(non_snake_case)]
|
||||
pub fn Form<A>(cx: Scope, props: FormProps<A>) -> Element
|
||||
where
|
||||
A: ToHref + 'static,
|
||||
A: TextProp + 'static,
|
||||
{
|
||||
let FormProps {
|
||||
method,
|
||||
@@ -62,7 +63,7 @@ where
|
||||
} = props;
|
||||
|
||||
let action_version = version;
|
||||
let action = use_resolved_path(cx, move || action.to_href()());
|
||||
let action = use_resolved_path(cx, move || action.to_value()());
|
||||
|
||||
let on_submit = move |ev: web_sys::SubmitEvent| {
|
||||
if ev.default_prevented() {
|
||||
@@ -131,15 +132,37 @@ where
|
||||
|
||||
let children = children();
|
||||
|
||||
view! { cx,
|
||||
<form
|
||||
method=method
|
||||
action=action
|
||||
enctype=enctype
|
||||
on:submit=on_submit
|
||||
>
|
||||
{children}
|
||||
</form>
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "stable")] {
|
||||
let on_submit = move |ev: web_sys::Event| on_submit(ev.unchecked_into());
|
||||
}
|
||||
};
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(not(feature = "stable"))] {
|
||||
view! { cx,
|
||||
<form
|
||||
method=method
|
||||
action=action
|
||||
enctype=enctype
|
||||
on:submit=on_submit
|
||||
>
|
||||
{children}
|
||||
</form>
|
||||
}
|
||||
}
|
||||
else {
|
||||
view! { cx,
|
||||
<form
|
||||
method=method
|
||||
action=move || action.get()
|
||||
enctype=enctype
|
||||
on:submit=on_submit
|
||||
>
|
||||
{children}
|
||||
</form>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,6 +305,12 @@ where
|
||||
|
||||
let children = (props.children)();
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "stable")] {
|
||||
let on_submit = move |ev: web_sys::Event| on_submit(ev.unchecked_into());
|
||||
}
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<form
|
||||
method="POST"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::leptos_dom::IntoChild;
|
||||
use leptos::*;
|
||||
use typed_builder::TypedBuilder;
|
||||
use leptos::typed_builder::*;
|
||||
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
use wasm_bindgen::JsCast;
|
||||
@@ -10,31 +10,31 @@ use crate::{use_location, use_resolved_path, State};
|
||||
|
||||
/// Describes a value that is either a static or a reactive URL, i.e.,
|
||||
/// a [String], a [&str], or a reactive `Fn() -> String`.
|
||||
pub trait ToHref {
|
||||
pub trait TextProp {
|
||||
/// Converts the (static or reactive) URL into a function that can be called to
|
||||
/// return the URL.
|
||||
fn to_href(&self) -> Box<dyn Fn() -> String + '_>;
|
||||
fn to_value(&self) -> Box<dyn Fn() -> String + '_>;
|
||||
}
|
||||
|
||||
impl ToHref for &str {
|
||||
fn to_href(&self) -> Box<dyn Fn() -> String> {
|
||||
impl TextProp for &str {
|
||||
fn to_value(&self) -> Box<dyn Fn() -> String> {
|
||||
let s = self.to_string();
|
||||
Box::new(move || s.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToHref for String {
|
||||
fn to_href(&self) -> Box<dyn Fn() -> String> {
|
||||
impl TextProp for String {
|
||||
fn to_value(&self) -> Box<dyn Fn() -> String> {
|
||||
let s = self.clone();
|
||||
Box::new(move || s.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> ToHref for F
|
||||
impl<F> TextProp for F
|
||||
where
|
||||
F: Fn() -> String + 'static,
|
||||
{
|
||||
fn to_href(&self) -> Box<dyn Fn() -> String + '_> {
|
||||
fn to_value(&self) -> Box<dyn Fn() -> String + '_> {
|
||||
Box::new(self)
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ where
|
||||
pub struct AProps<C, H>
|
||||
where
|
||||
C: IntoChild,
|
||||
H: ToHref + 'static,
|
||||
H: TextProp + 'static,
|
||||
{
|
||||
/// Used to calculate the link's `href` attribute. Will be resolved relative
|
||||
/// to the current route.
|
||||
@@ -56,26 +56,32 @@ where
|
||||
#[builder(default)]
|
||||
pub exact: bool,
|
||||
/// An object of any type that will be pushed to router state
|
||||
#[builder(default, setter(strip_option))]
|
||||
#[builder(default, setter(strip_option, into))]
|
||||
pub state: Option<State>,
|
||||
/// If `true`, the link will not add to the browser's history (so, pressing `Back`
|
||||
/// will skip this page.)
|
||||
#[builder(default)]
|
||||
pub replace: bool,
|
||||
/// Sets the `class` attribute on the underlying `<a>` tag, making it easier to style.
|
||||
#[builder(default, setter(strip_option, into))]
|
||||
pub class: Option<MaybeSignal<String>>,
|
||||
/// The nodes or elements to be shown inside the link.
|
||||
pub children: Box<dyn Fn() -> Vec<C>>,
|
||||
}
|
||||
|
||||
/// An HTML [`a`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a)
|
||||
/// progressively enhanced to use client-side routing.
|
||||
///
|
||||
/// Note that client-side routing also works with ordinary HTML `<a>` tags, although
|
||||
/// the `<A/>` component automatically resolves nested relative routes correctly.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn A<C, H>(cx: Scope, props: AProps<C, H>) -> Element
|
||||
where
|
||||
C: IntoChild,
|
||||
H: ToHref + 'static,
|
||||
H: TextProp + 'static,
|
||||
{
|
||||
let location = use_location(cx);
|
||||
let href = use_resolved_path(cx, move || props.href.to_href()());
|
||||
let href = use_resolved_path(cx, move || props.href.to_value()());
|
||||
let is_active = create_memo(cx, move |_| match href.get() {
|
||||
None => false,
|
||||
|
||||
@@ -99,6 +105,7 @@ where
|
||||
debug_warn!("[Link] Pass exactly one child to <A/>. If you want to pass more than one child, nest them within an element.");
|
||||
}
|
||||
let child = children.remove(0);
|
||||
let class = props.class;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
@@ -108,6 +115,7 @@ where
|
||||
prop:state={props.state.map(|s| s.to_js_value())}
|
||||
prop:replace={props.replace}
|
||||
aria-current=move || if is_active.get() { Some("page") } else { None }
|
||||
class=move || class.as_ref().map(|class| class.get())
|
||||
>
|
||||
{child}
|
||||
</a>
|
||||
@@ -117,6 +125,7 @@ where
|
||||
<a
|
||||
href=move || href().unwrap_or_default()
|
||||
aria-current=move || if is_active() { Some("page") } else { None }
|
||||
class=move || class.as_ref().map(|class| class.get())
|
||||
>
|
||||
{child}
|
||||
</a>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{borrow::Cow, rc::Rc};
|
||||
|
||||
use leptos::*;
|
||||
use typed_builder::TypedBuilder;
|
||||
use leptos::typed_builder::*;
|
||||
|
||||
use crate::{
|
||||
matching::{resolve_path, PathMatch, RouteDefinition, RouteMatch},
|
||||
|
||||
@@ -2,8 +2,8 @@ use cfg_if::cfg_if;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use leptos::*;
|
||||
use leptos::typed_builder::*;
|
||||
use thiserror::Error;
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
use wasm_bindgen::JsCast;
|
||||
@@ -68,7 +68,6 @@ impl std::fmt::Debug for RouterContextInner {
|
||||
f.debug_struct("RouterContextInner")
|
||||
.field("location", &self.location)
|
||||
.field("base", &self.base)
|
||||
.field("history", &std::any::type_name_of_val(&self.history))
|
||||
.field("cx", &self.cx)
|
||||
.field("reference", &self.reference)
|
||||
.field("set_reference", &self.set_reference)
|
||||
@@ -90,7 +89,10 @@ impl RouterContext {
|
||||
let history = use_context::<RouterIntegrationContext>(cx)
|
||||
.unwrap_or_else(|| RouterIntegrationContext(Rc::new(crate::BrowserIntegration {})));
|
||||
} else {
|
||||
let history = use_context::<RouterIntegrationContext>(cx).expect("You must call provide_context::<RouterIntegrationContext>(cx, ...) somewhere above the <Router/>.");
|
||||
let history = use_context::<RouterIntegrationContext>(cx).expect("You must call provide_context::<RouterIntegrationContext>(cx, ...) somewhere above the <Router/>.\n\n \
|
||||
If you are using `leptos_actix` or `leptos_axum` and seeing this message, it is a bug: \n \
|
||||
1. Please check to make sure you're on the latest versions of `leptos_actix` or `leptos_axum` and of `leptos_router`. \n
|
||||
2. If you're on the latest versions, please open an issue at https://github.com/gbj/leptos/issues");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -103,14 +105,16 @@ impl RouterContext {
|
||||
let base = base.unwrap_or_default();
|
||||
let base_path = resolve_path("", base, None);
|
||||
|
||||
if let Some(base_path) = &base_path && source.with(|s| s.value.is_empty()) {
|
||||
history.navigate(&LocationChange {
|
||||
value: base_path.to_string(),
|
||||
replace: true,
|
||||
scroll: false,
|
||||
state: State(None)
|
||||
});
|
||||
}
|
||||
if let Some(base_path) = &base_path {
|
||||
if source.with(|s| s.value.is_empty()) {
|
||||
history.navigate(&LocationChange {
|
||||
value: base_path.to_string(),
|
||||
replace: true,
|
||||
scroll: false,
|
||||
state: State(None),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// the current URL
|
||||
let (reference, set_reference) = create_signal(cx, source.with(|s| s.value.clone()));
|
||||
@@ -136,9 +140,9 @@ impl RouterContext {
|
||||
// 3) update the state
|
||||
// this will trigger the new route match below
|
||||
create_render_effect(cx, move |_| {
|
||||
let LocationChange { value, state, .. } = source();
|
||||
let LocationChange { value, state, .. } = source.get();
|
||||
cx.untrack(move || {
|
||||
if value != reference() {
|
||||
if value != reference.get() {
|
||||
set_reference.update(move |r| *r = value);
|
||||
set_state.update(move |s| *s = state);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
use std::{cmp::Reverse, rc::Rc, cell::{RefCell, Cell}, ops::IndexMut};
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
cmp::Reverse,
|
||||
ops::IndexMut,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use leptos::*;
|
||||
use typed_builder::TypedBuilder;
|
||||
use leptos::typed_builder::*;
|
||||
|
||||
use crate::{matching::{expand_optionals, join_paths, Branch, Matcher, RouteDefinition, get_route_matches, RouteMatch}, RouterContext, RouteContext};
|
||||
use crate::{
|
||||
matching::{
|
||||
expand_optionals, get_route_matches, join_paths, Branch, Matcher, RouteDefinition,
|
||||
RouteMatch,
|
||||
},
|
||||
RouteContext, RouterContext,
|
||||
};
|
||||
|
||||
/// Props for the [Routes] component, which contains route definitions and manages routing.
|
||||
#[derive(TypedBuilder)]
|
||||
@@ -13,8 +24,8 @@ pub struct RoutesProps {
|
||||
children: Box<dyn Fn() -> Vec<RouteDefinition>>,
|
||||
}
|
||||
|
||||
/// Contains route definitions and manages the actual routing process.
|
||||
///
|
||||
/// Contains route definitions and manages the actual routing process.
|
||||
///
|
||||
/// You should locate the `<Routes/>` component wherever on the page you want the routes to appear.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn Routes(cx: Scope, props: RoutesProps) -> impl IntoChild {
|
||||
@@ -34,9 +45,7 @@ pub fn Routes(cx: Scope, props: RoutesProps) -> impl IntoChild {
|
||||
// whenever path changes, update matches
|
||||
let matches = create_memo(cx, {
|
||||
let router = router.clone();
|
||||
move |_| {
|
||||
get_route_matches(branches.clone(), router.pathname().get())
|
||||
}
|
||||
move |_| get_route_matches(branches.clone(), router.pathname().get())
|
||||
});
|
||||
|
||||
// Rebuild the list of nested routes conservatively, and show the root route here
|
||||
@@ -68,61 +77,66 @@ pub fn Routes(cx: Scope, props: RoutesProps) -> impl IntoChild {
|
||||
let prev_match = prev_matches.and_then(|p| p.get(i));
|
||||
let next_match = next_matches.get(i).unwrap();
|
||||
|
||||
if let Some(prev) = prev_routes && let Some(prev_match) = prev_match && next_match.route.key == prev_match.route.key {
|
||||
let prev_one = { prev.borrow()[i].clone() };
|
||||
if i >= next.borrow().len() {
|
||||
next.borrow_mut().push(prev_one);
|
||||
} else {
|
||||
*(next.borrow_mut().index_mut(i)) = prev_one;
|
||||
}
|
||||
} else {
|
||||
equal = false;
|
||||
if i == 0 {
|
||||
root_equal.set(false);
|
||||
match (prev_routes, prev_match) {
|
||||
(Some(prev), Some(prev_match))
|
||||
if next_match.route.key == prev_match.route.key =>
|
||||
{
|
||||
let prev_one = { prev.borrow()[i].clone() };
|
||||
if i >= next.borrow().len() {
|
||||
next.borrow_mut().push(prev_one);
|
||||
} else {
|
||||
*(next.borrow_mut().index_mut(i)) = prev_one;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
equal = false;
|
||||
if i == 0 {
|
||||
root_equal.set(false);
|
||||
}
|
||||
|
||||
let disposer = cx.child_scope({
|
||||
let next = next.clone();
|
||||
let router = Rc::clone(&router.inner);
|
||||
move |cx| {
|
||||
let disposer = cx.child_scope({
|
||||
let next = next.clone();
|
||||
let next_ctx = RouteContext::new(
|
||||
cx,
|
||||
&RouterContext { inner: router },
|
||||
{
|
||||
let next = next.clone();
|
||||
move || {
|
||||
if let Some(route_states) = use_context::<Memo<RouterState>>(cx) {
|
||||
route_states.with(|route_states| {
|
||||
let routes = route_states.routes.borrow();
|
||||
routes.get(i + 1).cloned()
|
||||
})
|
||||
} else {
|
||||
next.borrow().get(i + 1).cloned()
|
||||
let router = Rc::clone(&router.inner);
|
||||
move |cx| {
|
||||
let next = next.clone();
|
||||
let next_ctx = RouteContext::new(
|
||||
cx,
|
||||
&RouterContext { inner: router },
|
||||
{
|
||||
let next = next.clone();
|
||||
move || {
|
||||
if let Some(route_states) =
|
||||
use_context::<Memo<RouterState>>(cx)
|
||||
{
|
||||
route_states.with(|route_states| {
|
||||
let routes = route_states.routes.borrow();
|
||||
routes.get(i + 1).cloned()
|
||||
})
|
||||
} else {
|
||||
next.borrow().get(i + 1).cloned()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
move || {
|
||||
matches.with(|m| m.get(i).cloned())
|
||||
}
|
||||
);
|
||||
},
|
||||
move || matches.with(|m| m.get(i).cloned()),
|
||||
);
|
||||
|
||||
if let Some(next_ctx) = next_ctx {
|
||||
if next.borrow().len() > i + 1 {
|
||||
next.borrow_mut()[i] = next_ctx;
|
||||
} else {
|
||||
next.borrow_mut().push(next_ctx);
|
||||
if let Some(next_ctx) = next_ctx {
|
||||
if next.borrow().len() > i + 1 {
|
||||
next.borrow_mut()[i] = next_ctx;
|
||||
} else {
|
||||
next.borrow_mut().push(next_ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if disposers.borrow().len() > i + 1 {
|
||||
let mut disposers = disposers.borrow_mut();
|
||||
let old_route_disposer = std::mem::replace(&mut disposers[i], disposer);
|
||||
old_route_disposer.dispose();
|
||||
} else {
|
||||
disposers.borrow_mut().push(disposer);
|
||||
if disposers.borrow().len() > i + 1 {
|
||||
let mut disposers = disposers.borrow_mut();
|
||||
let old_route_disposer = std::mem::replace(&mut disposers[i], disposer);
|
||||
old_route_disposer.dispose();
|
||||
} else {
|
||||
disposers.borrow_mut().push(disposer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,25 +148,34 @@ pub fn Routes(cx: Scope, props: RoutesProps) -> impl IntoChild {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(prev) = &prev && equal {
|
||||
RouterState {
|
||||
matches: next_matches.to_vec(),
|
||||
routes: prev_routes.cloned().unwrap_or_default(),
|
||||
root: prev.root.clone(),
|
||||
if let Some(prev) = &prev {
|
||||
if equal {
|
||||
RouterState {
|
||||
matches: next_matches.to_vec(),
|
||||
routes: prev_routes.cloned().unwrap_or_default(),
|
||||
root: prev.root.clone(),
|
||||
}
|
||||
} else {
|
||||
let root = next.borrow().get(0).cloned();
|
||||
RouterState {
|
||||
matches: next_matches.to_vec(),
|
||||
routes: Rc::new(RefCell::new(next.borrow().to_vec())),
|
||||
root,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let root = next.borrow().get(0).cloned();
|
||||
RouterState {
|
||||
matches: next_matches.to_vec(),
|
||||
routes: Rc::new(RefCell::new(next.borrow().to_vec())),
|
||||
root
|
||||
root,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// show the root route
|
||||
create_memo(cx, move |prev| {
|
||||
let root = create_memo(cx, move |prev| {
|
||||
provide_context(cx, route_states);
|
||||
route_states.with(|state| {
|
||||
let root = state.routes.borrow();
|
||||
@@ -162,14 +185,20 @@ pub fn Routes(cx: Scope, props: RoutesProps) -> impl IntoChild {
|
||||
}
|
||||
|
||||
if prev.is_none() || !root_equal.get() {
|
||||
root.as_ref().map(|route| {
|
||||
route.outlet().into_child(cx)
|
||||
})
|
||||
root.as_ref().map(|route| route.outlet().into_child(cx))
|
||||
} else {
|
||||
prev.cloned().unwrap()
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "stable")] {
|
||||
move || root.get()
|
||||
} else {
|
||||
root
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
|
||||
@@ -107,36 +107,51 @@ where
|
||||
fn into_param(value: Option<&str>, name: &str) -> Result<Self, ParamsError>;
|
||||
}
|
||||
|
||||
impl<T> IntoParam for Option<T>
|
||||
where
|
||||
T: FromStr,
|
||||
<T as FromStr>::Err: std::error::Error + 'static,
|
||||
{
|
||||
fn into_param(value: Option<&str>, _name: &str) -> Result<Self, ParamsError> {
|
||||
match value {
|
||||
None => Ok(None),
|
||||
Some(value) => match T::from_str(value) {
|
||||
Ok(value) => Ok(Some(value)),
|
||||
Err(e) => {
|
||||
eprintln!("{}", e);
|
||||
Err(ParamsError::Params(Rc::new(e)))
|
||||
}
|
||||
},
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(not(feature = "stable"))] {
|
||||
auto trait NotOption {}
|
||||
impl<T> !NotOption for Option<T> {}
|
||||
|
||||
impl<T> IntoParam for T
|
||||
where
|
||||
T: FromStr + NotOption,
|
||||
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
fn into_param(value: Option<&str>, name: &str) -> Result<Self, ParamsError> {
|
||||
let value = value.ok_or_else(|| ParamsError::MissingParam(name.to_string()))?;
|
||||
Self::from_str(value).map_err(|e| ParamsError::Params(Rc::new(e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto trait NotOption {}
|
||||
impl<T> !NotOption for Option<T> {}
|
||||
|
||||
impl<T> IntoParam for T
|
||||
where
|
||||
T: FromStr + NotOption,
|
||||
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
fn into_param(value: Option<&str>, name: &str) -> Result<Self, ParamsError> {
|
||||
let value = value.ok_or_else(|| ParamsError::MissingParam(name.to_string()))?;
|
||||
Self::from_str(value).map_err(|e| ParamsError::Params(Rc::new(e)))
|
||||
impl<T> IntoParam for Option<T>
|
||||
where
|
||||
T: FromStr,
|
||||
<T as FromStr>::Err: std::error::Error + 'static,
|
||||
{
|
||||
fn into_param(value: Option<&str>, _name: &str) -> Result<Self, ParamsError> {
|
||||
match value {
|
||||
None => Ok(None),
|
||||
Some(value) => match T::from_str(value) {
|
||||
Ok(value) => Ok(Some(value)),
|
||||
Err(e) => {
|
||||
eprintln!("{}", e);
|
||||
Err(ParamsError::Params(Rc::new(e)))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
impl<T> IntoParam for T
|
||||
where
|
||||
T: FromStr,
|
||||
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
fn into_param(value: Option<&str>, name: &str) -> Result<Self, ParamsError> {
|
||||
let value = value.ok_or_else(|| ParamsError::MissingParam(name.to_string()))?;
|
||||
Self::from_str(value).map_err(|e| ParamsError::Params(Rc::new(e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,9 @@
|
||||
//! them with server-side rendering (with or without hydration), they just work,
|
||||
//! whether JS/WASM have loaded or not.
|
||||
//!
|
||||
//! Note as well that client-side routing works with ordinary `<a>` tags, as well,
|
||||
//! so you do not even need to use the `<A/>` component in most cases.
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```rust
|
||||
@@ -135,10 +138,9 @@
|
||||
//!
|
||||
//! ```
|
||||
|
||||
#![feature(auto_traits)]
|
||||
#![feature(let_chains)]
|
||||
#![feature(negative_impls)]
|
||||
#![feature(type_name_of_val)]
|
||||
#![cfg_attr(not(feature = "stable"), feature(auto_traits))]
|
||||
#![cfg_attr(not(feature = "stable"), feature(negative_impls))]
|
||||
#![cfg_attr(not(feature = "stable"), feature(type_name_of_val))]
|
||||
|
||||
mod components;
|
||||
mod history;
|
||||
|
||||
@@ -80,13 +80,15 @@ impl Matcher {
|
||||
path.push_str(loc_segment);
|
||||
}
|
||||
|
||||
if let Some(splat) = &self.splat && !splat.is_empty() {
|
||||
let value = if len_diff > 0 {
|
||||
loc_segments[self.len..].join("/")
|
||||
} else {
|
||||
"".into()
|
||||
};
|
||||
params.insert(splat.into(), value);
|
||||
if let Some(splat) = &self.splat {
|
||||
if !splat.is_empty() {
|
||||
let value = if len_diff > 0 {
|
||||
loc_segments[self.len..].join("/")
|
||||
} else {
|
||||
"".into()
|
||||
};
|
||||
params.insert(splat.into(), value);
|
||||
}
|
||||
}
|
||||
|
||||
Some(PathMatch { path, params })
|
||||
|
||||
Reference in New Issue
Block a user