Compare commits

..

37 Commits

Author SHA1 Message Date
Greg Johnston
4360a73392 Fix SimpleCounter example 2022-12-09 14:57:58 -05:00
Greg Johnston
50b0fe157a Fix example test 2022-12-09 13:34:35 -05:00
Greg Johnston
64a5d75ec4 .into() calls were interfering with components that have generic props 2022-12-09 13:09:02 -05:00
Greg Johnston
baf3cc8712 Correct imports 2022-12-09 12:36:33 -05:00
Greg Johnston
23777ad67b Use leptos reexport of typed-builder crate 2022-12-09 12:30:21 -05:00
Greg Johnston
08be1ba622 Fix warnings 2022-12-08 19:28:23 -05:00
Greg Johnston
605398bcea Only use default for Option<T> 2022-12-08 19:27:45 -05:00
Greg Johnston
aca2c131d4 Add the ability to document Component and ComponentProps in a single doc comment. 2022-12-08 17:08:54 -05:00
Greg Johnston
9d950b97ff Better error message for RouterIntegrationContext 2022-12-07 07:52:01 -05:00
Greg Johnston
f6a299ae3c Merge pull request #154 from gbj/fix-component-siblings-in-hydration
Fix issue #109
2022-12-07 00:06:48 -05:00
Greg Johnston
1ba602ec47 Fix issue #109 2022-12-06 22:31:54 -05:00
Greg Johnston
1f3dde5b4a Fix Hackernews CSS 2022-12-06 19:22:29 -05:00
Greg Johnston
a65cd67db3 Fix name of Wasm export 2022-12-06 18:18:46 -05:00
Greg Johnston
bacd99260b Fix benchmarks 2022-12-06 18:18:38 -05:00
Greg Johnston
2b726f1a88 Fix docs on props for each component 2022-12-06 11:42:47 -05:00
Greg Johnston
5c45538e9f Make necessary changes for stable support for router and meta 2022-12-05 18:55:03 -05:00
Greg Johnston
7f696a9ac4 support 2022-12-05 17:25:02 -05:00
Greg Johnston
bcd6e671f7 0.0.20 2022-12-05 17:23:22 -05:00
Greg Johnston
7a72f127de Stable compatibility 2022-12-05 17:18:17 -05:00
Greg Johnston
2ff5ec21c8 0.0.20 2022-12-05 16:25:16 -05:00
Greg Johnston
a1f94b609f Improvements to example to show off transitions and streaming 2022-12-05 16:17:47 -05:00
Greg Johnston
da5034da33 Bump versions after WASM-less fix 2022-12-05 16:17:29 -05:00
Greg Johnston
0c509970b5 Fix ability of server functions to work without WASM 2022-12-05 16:17:15 -05:00
Greg Johnston
d894c4dcf9 Merge branch 'main' of https://github.com/gbj/leptos 2022-12-05 16:10:33 -05:00
Greg Johnston
dc15184781 Merge pull request #152 from benwis/cargo-leptos-updates
Add config crate and generate file for cargo-leptos to watch
2022-12-05 12:04:56 -05:00
Ben Wishovich
3200068ab3 Doc tweaks 2022-12-04 18:11:20 -08:00
Ben Wishovich
0a9da8d55e Add some doc comments, and change the behavior of the reload_port 2022-12-04 17:55:51 -08:00
Ben Wishovich
52ad546710 Update rest of the examples and make the tests pass 2022-12-04 17:25:03 -08:00
Ben Wishovich
f88d2fa56a Add socket_address option to configure the ip address and port to serve 2022-12-04 15:50:29 -08:00
Ben Wishovich
f63cb02277 Commit WIP version of common config struct that writes a KDL file for cargo-leptos 2022-12-04 14:50:36 -08:00
Greg Johnston
4b363f9b33 0.0.3 for axum 0.6 compatibility 2022-12-03 22:12:17 -05:00
Ben Wishovich
7b376b6d3a Draft Builder Pattern for Render Options to add Leptos Autorender Code 2022-12-02 16:33:59 -08:00
Ben Wishovich
8fbb4abc76 Switch integrations to pass in a full path and name v the name to enable different pkg structures 2022-12-02 12:01:51 -08:00
Greg Johnston
d0ff64daaa Merge pull request #149 from gbj/a-tag-class-helper
Allow styling `<A/>` tags with `class` property
2022-12-02 14:09:10 -05:00
Greg Johnston
bb97234817 Merge pull request #148 from gbj/explicit-stable-not-required
Automatically enable the `stable` feature if you're on `stable` Rust
2022-12-02 14:08:22 -05:00
Greg Johnston
19698d86b6 Allow styling <A/> component with class 2022-12-02 13:20:07 -05:00
Greg Johnston
21ef96806f Rename ToHref to something a little more generic 2022-12-02 13:04:37 -05:00
54 changed files with 782 additions and 1682 deletions

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ blob.rs
Cargo.lock
**/*.rs.bk
.DS_Store
.leptos.kdl

View File

@@ -4,6 +4,7 @@ members = [
"leptos",
"leptos_dom",
"leptos_core",
"leptos_config",
"leptos_macro",
"leptos_reactive",
"leptos_server",

View File

@@ -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);

View File

@@ -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();

View File

@@ -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)/>

View File

@@ -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
}

View File

@@ -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>
}
}

View File

@@ -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/> })
}

View File

@@ -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();

View File

@@ -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`

View File

@@ -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"]]

View File

@@ -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| {

View File

@@ -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
}
}
}

View File

@@ -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">

View File

@@ -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
}

View File

@@ -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();

View File

@@ -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>

View File

@@ -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
}

View File

@@ -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>
}

View File

@@ -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>

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_actix"
version = "0.0.1"
version = "0.0.2"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

@@ -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(

View File

@@ -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",
] }

View File

@@ -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>";

View File

@@ -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"]

View File

@@ -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
View 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
View 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,
}
}
}

View File

@@ -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"

View File

@@ -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>,

View File

@@ -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.

View File

@@ -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,

View File

@@ -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"

View File

@@ -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)]

View File

@@ -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]

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_reactive"
version = "0.0.19"
version = "0.0.20"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

@@ -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"

View File

@@ -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"]

View File

@@ -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

View File

@@ -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

View File

@@ -50,10 +50,10 @@ where
pub struct TitleProps {
/// A function that will be applied to any text value before its 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 documents title by creating an [HTMLTitleElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTitleElement).

View File

@@ -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"]

View File

@@ -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"

View File

@@ -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>

View File

@@ -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},

View File

@@ -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);
}

View File

@@ -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)]

View File

@@ -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)))
}
}
}
}

View File

@@ -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;

View File

@@ -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 })