Compare commits

..

68 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
Greg Johnston
70e18d2aeb Automatically enable the stable feature if you're on stable Rust 2022-12-02 12:56:05 -05:00
Greg Johnston
5152703f0c Clear warnings 2022-12-02 12:39:32 -05:00
Greg Johnston
3d54055573 Add <Meta/> component to leptos_meta 2022-12-02 12:36:51 -05:00
Greg Johnston
a5b99a3e40 Merge branches 'main' and 'main' of https://github.com/gbj/leptos 2022-12-01 21:42:02 -05:00
Greg Johnston
101e65b724 Does adding skip_feature_sets here help with CI problem? 2022-12-01 21:41:58 -05:00
Greg Johnston
a3f91604b9 Merge pull request #141 from benwis/axum-0.6
Update Axum examples to latest 0.6 release and streamline them a bit
2022-12-01 17:23:20 -05:00
Ben Wishovich
f457d8f319 Fix doc test 2022-12-01 12:56:27 -08:00
Greg Johnston
58abe55d7b Merge branch 'main' into axum-0.6 2022-12-01 13:10:06 -05:00
Greg Johnston
634ac17095 Merge pull request #144 from Indrazar/main
update functions for Windows file directories
2022-12-01 12:43:19 -05:00
Ben Wishovich
79faad4aac Missed another couple imports 2022-11-30 22:41:31 -08:00
IcosaHedron
cedc68c341 remove debug string from axum integration 2022-11-30 23:20:14 -05:00
indrazar
8ec772a129 update functions for Windows file directories
- leptos_macro/src/server.rs server_macro_impl
 - integrations/axum/src/lib.rs handle_server_fns
2022-11-30 23:01:59 -05:00
Greg Johnston
8d671866a3 Merge pull request #142 from FDiskas/patch-1
Update example lib.rs
2022-11-30 20:47:24 -05:00
Ben Wishovich
2edc5b3b8b Remove extra print 2022-11-30 17:31:14 -08:00
Vytenis
be96a230ee Update lib.rs 2022-12-01 01:47:54 +02:00
Ben Wishovich
0f8930b6f2 Update Axum examples to latest 0.6 release and streamline things 2022-11-30 15:02:22 -08:00
Greg Johnston
2b5c4abac5 Merge pull request #140 from gbj/transition-component
Transition component
2022-11-30 16:20:02 -05:00
Greg Johnston
db8c393f49 Update examples 2022-11-30 11:36:54 -05:00
Greg Johnston
f18a7b35f2 Use SignalSetter in <Transition/> API 2022-11-30 11:36:50 -05:00
Greg Johnston
a2c5855362 <Transition/> component 2022-11-30 11:27:07 -05:00
Greg Johnston
644d097cb6 Fix SignalSetter tests 2022-11-30 11:22:05 -05:00
Greg Johnston
9c0be9e317 Finishing implementing SignalSetter wrapper. 2022-11-30 07:46:04 -05:00
Greg Johnston
5faa2efa2d Merge pull request #137 from benwis/example_readmes
Add READMEs to all examples and fix typo in todo-app-axum
2022-11-29 20:00:36 -05:00
Greg Johnston
c5a1e9a447 Copy edited and added Trunk install instructions 2022-11-29 20:00:09 -05:00
Ben Wishovich
e88e131ec3 Add READMEs to all examples and fix typo in todo-app-axum 2022-11-29 13:14:59 -08:00
Greg Johnston
80df7a0dac Merge pull request #135 from ghassanachi/patch-1
Update `counters` example link in docs
2022-11-29 14:48:40 -05:00
Ghassan Gedeon Achi
493f05fda1 Update counters example link in docs 2022-11-29 11:51:27 -07:00
Greg Johnston
4578622b6f Merge pull request #134 from gbj/fix-router-hydration-panic
Fix out-of-order hydration issue
2022-11-29 08:56:03 -05:00
Greg Johnston
c7dd6200e8 Fix GTK example 2022-11-29 07:07:10 -05:00
Greg Johnston
6e20f31df1 Fix out-of-order hydration issue by removing old code that was handling this in an incorrect way 2022-11-29 07:06:25 -05:00
Greg Johnston
5f58db40f0 Merge pull request #131 from gbj/fix-3x-server-resource-fetching
Fix issue in which server-side resource are called 3x
2022-11-29 06:14:22 -05:00
93 changed files with 1562 additions and 1969 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

@@ -10,7 +10,7 @@ To run it as a server side app with hydration, first you should run
wasm-pack build --target=web --no-default-features --features=hydrate
```
to generate the Webassembly to provide hydration features for the server.
to generate the WebAssembly to provide hydration features for the server.
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
```bash

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

@@ -2,5 +2,6 @@
This example creates a simple counter in a client side rendered app with Rust and WASM!
To run it, just issue the `trunk serve --open` command in the example root. This will build the app, run it, and open a new browser to serve it.
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)

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

@@ -0,0 +1,10 @@
# Leptos Counters Example on Rust Stable
This example showcases a basic Leptos app with many counters. It is a good example of how to setup a basic reactive app with signals and effects, and how to interact with browser events. Unlike the other counters example, it will compile on Rust stable, because it has the `stable` feature enabled.
## Client Side Rendering
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CSR bundle.
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)

View File

@@ -0,0 +1,10 @@
# Leptos Counters Example
This example showcases a basic Leptos app with many counters. It is a good example of how to set up a basic reactive app with signals and effects, and how to interact with browser events.
## Client Side Rendering
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CSR bundle.
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)

View File

@@ -11,7 +11,7 @@ serde = { version = "1", features = ["derive"] }
log = "0.4"
console_log = "0.2"
console_error_panic_hook = "0.1.7"
gloo-timers = { version = "0.2", features = ["futures"] }
[dev-dependencies]
wasm-bindgen-test = "0.3.0"

10
examples/fetch/README.md Normal file
View File

@@ -0,0 +1,10 @@
# Client Side Fetch
This example shows how to fetch data from the client in WebAssembly.
## Client Side Rendering
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CSR bundle.
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)

View File

@@ -1,5 +1,6 @@
use std::time::Duration;
use gloo_timers::future::TimeoutFuture;
use leptos::*;
use serde::{Deserialize, Serialize};
@@ -9,6 +10,10 @@ pub struct Cat {
}
async fn fetch_cats(count: u32) -> Result<Vec<String>, ()> {
// artificial delay
// the cat API is too fast to show the transition
TimeoutFuture::new(500).await;
if count > 0 {
let res = reqwasm::http::Request::get(&format!(
"https://api.thecatapi.com/v1/images/search?limit={}",
@@ -32,8 +37,9 @@ async fn fetch_cats(count: u32) -> Result<Vec<String>, ()> {
pub fn fetch_example(cx: Scope) -> web_sys::Element {
let (cat_count, set_cat_count) = create_signal::<u32>(cx, 1);
let cats = create_resource(cx, cat_count, |count| fetch_cats(count));
let (pending, set_pending) = create_signal(cx, false);
view! { cx,
view! { cx,
<div>
<label>
"How many cats would you like?"
@@ -45,16 +51,22 @@ pub fn fetch_example(cx: Scope) -> web_sys::Element {
}
/>
</label>
{move || pending().then(|| view! { cx, <p>"Loading more cats..."</p> })}
<div>
<Suspense fallback={"Loading (Suspense Fallback)...".to_string()}>
// <Transition/> holds the previous value while new async data is being loaded
// Switch the <Transition/> to <Suspense/> to fall back to "Loading..." every time
<Transition
fallback={"Loading (Suspense Fallback)...".to_string()}
set_pending
>
{move || {
cats.read().map(|data| match data {
Err(_) => view! { cx, <pre>"Error"</pre> },
Ok(cats) => view! { cx,
Ok(cats) => view! { cx,
<div>{
cats.iter()
.map(|src| {
view! { cx,
view! { cx,
<img src={src}/>
}
})
@@ -64,7 +76,7 @@ pub fn fetch_example(cx: Scope) -> web_sys::Element {
})
}
}
</Suspense>
</Transition>
</div>
</div>
}

8
examples/gtk/README.md Normal file
View File

@@ -0,0 +1,8 @@
# Leptos in a GTK App
This example creates a basic GTK app that uses Leptoss reactive primitives.
## Build and Run
Unlike the other examples, this has a variety of build prerequisites that are out of scope of this crate. More detail on that can be found [here](https://gtk-rs.org/gtk4-rs/stable/latest/book/installation.html). The example comes from [here](https://gtk-rs.org/gtk4-rs/stable/latest/book/hello_world.html) and should be
runnable with `cargo run` if you have the GTK prerequisites installed.

View File

@@ -6,7 +6,7 @@ const APP_ID: &str = "dev.leptos.Counter";
// Basic GTK app setup from https://gtk-rs.org/gtk4-rs/stable/latest/book/hello_world.html
fn main() {
_ = create_scope(|cx| {
_ = create_scope(create_runtime(), |cx| {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();

View File

@@ -7,27 +7,27 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
anyhow = "1"
console_log = "0.2"
console_error_panic_hook = "0.1"
futures = "0.3"
cfg-if = "1"
anyhow = "1.0.66"
console_log = "0.2.0"
console_error_panic_hook = "0.1.7"
futures = "0.3.25"
cfg-if = "1.0.0"
leptos = { path = "../../leptos", default-features = false, features = [
"serde",
] }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_meta = { path = "../../meta", default-features = false }
leptos_router = { path = "../../router", default-features = false }
log = "0.4"
simple_logger = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
gloo-net = { version = "0.2", features = ["http"] }
reqwest = { version = "0.11", features = ["json"] }
axum = { version = "0.5.17", optional = true }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
serde_json = "1.0.89"
gloo-net = { version = "0.2.5", features = ["http"] }
reqwest = { version = "0.11.13", features = ["json"] }
axum = { version = "0.6.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.3.4", features = ["fs"], optional = true }
tokio = { version = "1.0", features = ["full"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.8", optional = true }
[features]

View File

@@ -3,18 +3,27 @@
This example creates a basic clone of the Hacker News site. It showcases Leptos' ability to create both a client-side rendered app, and a server side rendered app with hydration, in a single repository. This repo differs from the main Hacker News example by using Axum as it's server.
## Client Side Rendering
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CRS bundle
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CSR bundle.
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
## Server Side Rendering With Hydration
To run it as a server side app with hydration, first you should run
To run it as a server side app with hydration, first you should run
```bash
wasm-pack build --target=web --no-default-features --features=hydrate
```
to generate the Webassembly to provide hydration features for the server.
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
to generate the WebAssembly to hydrate the HTML that is generated on the server.
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
```bash
cargo run --no-default-features --features=ssr
```
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above
> This should be temporary, and vastly improve once cargo-leptos becomes ready for prime time!

View File

@@ -1,66 +0,0 @@
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
http::{Request, Response, StatusCode, Uri},
};
use tower::ServiceExt;
use tower_http::services::ServeDir;
pub async fn file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
let res = get_static_file(uri.clone(), "/pkg").await?;
println!("FIRST URI{:?}", uri);
if res.status() == StatusCode::NOT_FOUND {
// try with `.html`
// TODO: handle if the Uri has query parameters
match format!("{}.html", uri).parse() {
Ok(uri_html) => get_static_file(uri_html, "/pkg").await,
Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string())),
}
} else {
Ok(res)
}
}
pub async fn get_static_file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
let res = get_static_file(uri.clone(), "/static").await?;
println!("FIRST URI{:?}", uri);
if res.status() == StatusCode::NOT_FOUND {
Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string()))
} else {
Ok(res)
}
}
async fn get_static_file(uri: Uri, base: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(&uri).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// When run normally, the root should be the crate root
println!("Base: {:#?}", base);
if base == "/static" {
match ServeDir::new("./static").oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
))
}
} else if base == "/pkg" {
match ServeDir::new("./pkg").oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
} else{
Err((StatusCode::NOT_FOUND, "Not Found".to_string()))
}
}
}
}

View File

@@ -3,7 +3,6 @@ use leptos::*;
use leptos_meta::*;
use leptos_router::*;
mod api;
pub mod handlers;
mod routes;
use routes::nav::*;
use routes::stories::*;

View File

@@ -4,32 +4,47 @@ use leptos::*;
// boilerplate to run in different modes
cfg_if! {
if #[cfg(feature = "ssr")] {
// use actix_files::{Files, NamedFile};
// use actix_web::*;
use axum::{
routing::{get},
Router,
handler::Handler,
error_handling::HandleError,
};
use http::StatusCode;
use std::net::SocketAddr;
use leptos_hackernews_axum::handlers::{file_handler, get_static_file_handler};
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}");
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
// These are Tower Services that will serve files from the static and pkg repos.
// HandleError is needed as Axum requires services to implement Infallible Errors
// because all Errors are converted into Responses
let static_service = HandleError::new( ServeDir::new("./static"), handle_file_error);
let pkg_service =HandleError::new( ServeDir::new("./pkg"), handle_file_error);
/// Convert the Errors from ServeDir to a type that implements IntoResponse
async fn handle_file_error(err: std::io::Error) -> (StatusCode, String) {
(
StatusCode::NOT_FOUND,
format!("File Not Found: {}", err),
)
}
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("/pkg", get(file_handler))
.nest("/static", get(get_static_file_handler))
.fallback(leptos_axum::render_app_to_stream("leptos_hackernews_axum", |cx| view! { cx, <App/> }).into_service());
.nest_service("/pkg", pkg_service)
.nest_service("/static", static_service)
.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

@@ -1,20 +1,29 @@
# Leptos Hacker News Example
This example creates a basic clone of the Hacker News site. It showcases Leptos' ability to create both a client-side rendered app, and a server side rendered app with hydration, in a single repository
This example creates a basic clone of the Hacker News site. It showcases Leptoss ability to create both a client-side rendered app, and a server side rendered app with hydration, in a single repository. It uses Actix as its backend.
## Client Side Rendering
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CRS bundle
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CSR bundle.
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
## Server Side Rendering With Hydration
To run it as a server side app with hydration, first you should run
To run it as a server side app with hydration, first you should run
```bash
wasm-pack build --target=web --no-default-features --features=hydrate
```
to generate the Webassembly to provide hydration features for the server.
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
to generate the WebAssembly to hydrate the HTML that is generated on the server.
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
```bash
cargo run --no-default-features --features=ssr
```
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above
> This should be temporary, and vastly improve once cargo-leptos becomes ready for prime time!

View File

@@ -16,7 +16,8 @@ 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 />
<main>
@@ -37,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

@@ -35,8 +35,10 @@ pub fn Stories(cx: Scope) -> Element {
api::fetch_api::<Vec<api::Story>>(cx, &api::story(&path)).await
},
);
let (pending, set_pending) = create_signal(cx, false);
let hide_more_link = move || stories.read().unwrap_or(None).unwrap_or_default().len() < 28;
let hide_more_link =
move || pending() || stories.read().unwrap_or(None).unwrap_or_default().len() < 28;
view! {
cx,
@@ -76,7 +78,10 @@ pub fn Stories(cx: Scope) -> Element {
</div>
<main class="news-list">
<div>
<Suspense fallback=view! { cx, <p>"Loading..."</p> }>
<Transition
fallback=view! { cx, <p>"Loading..."</p> }
set_pending
>
{move || match stories.read() {
None => None,
Some(None) => Some(view! { cx, <p>"Error loading stories."</p> }),
@@ -94,7 +99,7 @@ pub fn Stories(cx: Scope) -> Element {
})
}
}}
</Suspense>
</Transition>
</div>
</main>
</div>

View File

@@ -1,5 +1,6 @@
use crate::api;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
#[component]
@@ -16,9 +17,11 @@ pub fn Story(cx: Scope) -> Element {
}
},
);
let meta_description = move || story.read().and_then(|story| story.map(|story| story.title.clone())).unwrap_or_else(|| "Loading story...".to_string());
view! { cx,
<div>
<Meta name="description" content=meta_description/>
{move || story.read().map(|story| match story {
None => view! { cx, <div class="item-view">"Error loading this story."</div> },
Some(story) => view! { cx,

View File

@@ -0,0 +1,17 @@
# Parent Child Example
This example highlights four different ways that child components can communicate with their parent:
1. <ButtonA/>: passing a WriteSignal as one of the child component props,
for the child component to write into and the parent to read
2. <ButtonB/>: passing a closure as one of the child component props, for
the child component to call
3. <ButtonC/>: adding a simple event listener on the child component itself
4. <ButtonD/>: providing a context that is used in the component (rather than prop drilling)
## Client Side Rendering
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CSR bundle
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)

View File

@@ -1,8 +1,11 @@
# Leptos Router Example
This example demonstrates how Leptos' router works
This example demonstrates how Leptoss router for client side routing.
## Build and Run it
## Run it
```bash
trunk serve --open
```
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)

View File

@@ -143,7 +143,7 @@ pub fn Settings(_cx: Scope) -> Element {
<fieldset>
<legend>"Name"</legend>
<input type="text" name="first_name" placeholder="First"/>
<input type="text" name="first_name" placeholder="Last"/>
<input type="text" name="last_name" placeholder="Last"/>
</fieldset>
<pre>"This page is just a placeholder."</pre>
</form>

View File

@@ -1,6 +1,6 @@
# Leptos Counter Isomorphic Example
# Leptos Todo App Sqlite with CBOR
This example demonstrates how to use a server functions and multi-actions to build a simple todo app.
This example creates a basic todo app with an Actix backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server. It is identical to the todo-app-sqlite example, but utilizes CBOR encoding for one of the server functions
## Server Side Rendering With Hydration
@@ -10,11 +10,12 @@ To run it as a server side app with hydration, first you should run
wasm-pack build --target=web --no-default-features --features=hydrate
```
to generate the Webassembly to provide hydration features for the server.
to generate the WebAssembly to hydrate the HTML that is generated on the server.
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
```bash
cargo run
cargo run --no-default-features --features=ssr
```
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above

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

@@ -7,29 +7,29 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
anyhow = "1"
console_log = "0.2"
console_error_panic_hook = "0.1"
futures = "0.3"
cfg-if = "1"
anyhow = "1.0.66"
console_log = "0.2.0"
console_error_panic_hook = "0.1.7"
futures = "0.3.25"
cfg-if = "1.0.0"
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
"serde",
] }
leptos_axum = { path = "../../../leptos/integrations/axum", default-features = false, optional = true }
leptos_meta = { path = "../../../leptos/meta", default-features = false }
leptos_router = { path = "../../../leptos/router", default-features = false }
log = "0.4"
simple_logger = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
gloo-net = { version = "0.2", features = ["http"] }
reqwest = { version = "0.11", features = ["json"] }
axum = { version = "0.5.17", optional = true }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
serde_json = "1.0.89"
gloo-net = { version = "0.2.5", features = ["http"] }
reqwest = { version = "0.11.13", features = ["json"] }
axum = { version = "0.6.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.3.4", features = ["fs"], optional = true }
tokio = { version = "1.0", features = ["full"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.8", optional = true }
sqlx = { version = "0.6", features = [
sqlx = { version = "0.6.2", features = [
"runtime-tokio-rustls",
"sqlite",
], optional = true }

View File

@@ -1,21 +1,22 @@
# Leptos Todo App Sqlite with Axum
This example creates a basic todo app with an Axum backend that uses Leptos' server functions to call sqlx from the client and seamlessly
run it on the server
## Client Side Rendering
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CRS bundle
This example creates a basic todo app with an Axum backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server.
## Server Side Rendering With Hydration
To run it as a server side app with hydration, first you should run
To run it as a server side app with hydration, first you should run
```bash
wasm-pack build --target=web --no-default-features --features=hydrate
```
to generate the Webassembly to provide hydration features for the server.
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
to generate the WebAssembly to hydrate the HTML that is generated on the server.
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
```bash
cargo run --no-default-features --features=ssr
```
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above
> This should be temporary, and vastly improve once cargo-leptos becomes ready for prime time!
> This should be temporary, and vastly improve once cargo-leptos becomes ready for prime time!

View File

@@ -1,63 +0,0 @@
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
http::{Request, Response, StatusCode, Uri},
};
use tower::ServiceExt;
use tower_http::services::ServeDir;
pub async fn file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
let res = get_static_file(uri.clone(), "/pkg").await?;
if res.status() == StatusCode::NOT_FOUND {
// try with `.html`
// TODO: handle if the Uri has query parameters
match format!("{}.html", uri).parse() {
Ok(uri_html) => get_static_file(uri_html, "/pkg").await,
Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string())),
}
} else {
Ok(res)
}
}
pub async fn get_static_file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
let res = get_static_file(uri.clone(), "/static").await?;
if res.status() == StatusCode::NOT_FOUND {
Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string()))
} else {
Ok(res)
}
}
async fn get_static_file(uri: Uri, base: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(&uri).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// When run normally, the root should be the crate root
if base == "/static" {
match ServeDir::new("./static").oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
))
}
} else if base == "/pkg" {
match ServeDir::new("./pkg").oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
} else{
Err((StatusCode::NOT_FOUND, "Not Found".to_string()))
}
}
}
}

View File

@@ -1,6 +1,5 @@
use cfg_if::cfg_if;
use leptos::*;
pub mod handlers;
pub mod todo;
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.

View File

@@ -5,18 +5,20 @@ use leptos::*;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
routing::{get, post},
routing::{post},
error_handling::HandleError,
Router,
handler::Handler,
};
use std::net::SocketAddr;
use crate::todo::*;
use todo_app_sqlite_axum::handlers::{file_handler, get_static_file_handler};
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");
@@ -29,17 +31,34 @@ if #[cfg(feature = "ssr")] {
crate::todo::register_server_functions();
// These are Tower Services that will serve files from the static and pkg repos.
// HandleError is needed as Axum requires services to implement Infallible Errors
// because all Errors are converted into Responses
let static_service = HandleError::new( ServeDir::new("./static"), handle_file_error);
let pkg_service = HandleError::new( ServeDir::new("./pkg"), handle_file_error);
/// Convert the Errors from ServeDir to a type that implements IntoResponse
async fn handle_file_error(err: std::io::Error) -> (StatusCode, String) {
(
StatusCode::NOT_FOUND,
format!("File Not Found: {}", err),
)
}
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/*path", post(leptos_axum::handle_server_fns))
.nest("/pkg", get(file_handler))
.nest("/static", get(get_static_file_handler))
.fallback(leptos_axum::render_app_to_stream("todo_app_sqlite_axum", |cx| view! { cx, <TodoApp/> }).into_service());
.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(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

@@ -7,25 +7,25 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["openssl", "macros"] }
anyhow = "1"
broadcaster = "1"
console_log = "0.2"
console_error_panic_hook = "0.1"
serde = { version = "1", features = ["derive"] }
futures = "0.3"
cfg-if = "1"
actix-files = { version = "0.6.2", optional = true }
actix-web = { version = "4.2.1", optional = true, features = ["openssl", "macros"] }
anyhow = "1.0.66"
broadcaster = "1.0.0"
console_log = "0.2.0"
console_error_panic_hook = "0.1.7"
serde = { version = "1.0.148", features = ["derive"] }
futures = "0.3.25"
cfg-if = "1.0.0"
leptos = { path = "../../leptos", default-features = false, features = [
"serde",
] }
leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_meta = { path = "../../meta", default-features = false }
leptos_router = { path = "../../router", default-features = false }
log = "0.4"
simple_logger = "2"
log = "0.4.17"
simple_logger = "4.0.0"
gloo = { git = "https://github.com/rustwasm/gloo" }
sqlx = { version = "0.6", features = [
sqlx = { version = "0.6.2", features = [
"runtime-tokio-rustls",
"sqlite",
], optional = true }

View File

@@ -1,6 +1,6 @@
# Leptos Counter Isomorphic Example
# Leptos Todo App Sqlite
This example demonstrates how to use a server functions and multi-actions to build a simple todo app.
This example creates a basic todo app with an Actix backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server
## Server Side Rendering With Hydration
@@ -10,11 +10,12 @@ To run it as a server side app with hydration, first you should run
wasm-pack build --target=web --no-default-features --features=hydrate
```
to generate the Webassembly to provide hydration features for the server.
to generate the WebAssembly to hydrate the HTML that is generated on the server.
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
```bash
cargo run
cargo run --no-default-features --features=ssr
```
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above

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

@@ -0,0 +1,10 @@
# Leptos TodoMVC
This is a Leptos implementation of the TodoMVC example common to many frameworks. This is a relatively-simple application but shows off features like interaction between components and state management.
## Client Side Rendering
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CSR bundle.
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)

View File

@@ -0,0 +1,10 @@
# Leptos View Tests
This is a collection of mostly internal view tests for Leptos. Feel free to look if curious to see a variety of ways you can build identical views!
## Client Side Rendering
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CSR bundle.
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)

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"
@@ -8,8 +8,10 @@ repository = "https://github.com/gbj/leptos"
description = "Axum integrations for the Leptos web framework."
[dependencies]
axum = "0.5"
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

@@ -1,5 +1,5 @@
use axum::{
body::{Body, BoxBody, Bytes, Full, HttpBody, StreamBody},
body::{Body, Bytes, Full, StreamBody},
extract::Path,
http::{HeaderMap, HeaderValue, Request, StatusCode},
response::{IntoResponse, Response},
@@ -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].
///
@@ -46,8 +45,10 @@ pub async fn handle_server_fns(
// req: Request<Body>,
) -> impl IntoResponse {
// Axum Path extractor doesn't remove the first slash from the path, while Actix does
let fn_name = fn_name.replace("/", "");
println!("Body: {:#?}", &body);
let fn_name: String = match fn_name.strip_prefix("/") {
Some(path) => path.to_string(),
None => fn_name,
};
let (tx, rx) = futures::channel::oneshot::channel();
std::thread::spawn({
@@ -74,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
@@ -139,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]
@@ -151,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/> }).into_service());
/// .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`
@@ -165,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>,
@@ -176,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
@@ -189,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>";
@@ -219,9 +254,7 @@ pub fn render_app_to_stream(
};
provide_context(
cx,
RouterIntegrationContext::new(
integration,
),
RouterIntegrationContext::new(integration),
);
provide_context(cx, MetaContext::new());
let app = app_fn(cx);

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,41 +9,45 @@ 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"
[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"]
@@ -51,4 +55,30 @@ miniserde = ["leptos_reactive/miniserde"]
interning = ["leptos_dom/interning"]
[package.metadata.cargo-all-features]
denylist = ["stable"]
denylist = ["stable", "interning"]
skip_feature_sets = [
[
"csr",
"ssr",
],
[
"csr",
"hydrate",
],
[
"ssr",
"hydrate",
],
[
"serde",
"serde-lite",
],
[
"serde-lite",
"miniserde",
],
[
"serde",
"miniserde",
],
]

12
leptos/build.rs Normal file
View File

@@ -0,0 +1,12 @@
use rustc_version::{version, version_meta, Channel};
fn main() {
assert!(version().unwrap().major >= 1);
match version_meta().unwrap().channel {
Channel::Stable => {
println!("cargo:rustc-cfg=feature=\"stable\"")
}
_ => {}
}
}

View File

@@ -27,8 +27,12 @@
//! the [examples](https://github.com/gbj/leptos/tree/main/examples):
//! - [`counter`](https://github.com/gbj/leptos/tree/main/examples/counter) is the classic
//! counter example, showing the basics of client-side rendering and reactive DOM updates
//! - [`counters`](https://github.com/gbj/leptos/tree/main/examples/counter) introduces parent-child
//! - [`counter-isomorphic`](https://github.com/gbj/leptos/tree/main/examples/counter-isomorphic) is the classic
//! counter example run on the server using an isomorphic function, showing the basics of client-side rendering and reactive DOM updates
//! - [`counters`](https://github.com/gbj/leptos/tree/main/examples/counters) introduces parent-child
//! communication via contexts, and the `<For/>` component for efficient keyed list updates.
//! - [`counters-stable`](https://github.com/gbj/leptos/tree/main/examples/counters-stable) introduces parent-child
//! communication via contexts, and the `<For/>` component for efficient keyed list updates. Unlike counters, this compiles in Rust stable.
//! - [`parent-child`](https://github.com/gbj/leptos/tree/main/examples/parent-child) shows four different
//! ways a parent component can communicate with a child, including passing a closure, context, and more
//! - [`todomvc`](https://github.com/gbj/leptos/tree/main/examples/todomvc) implements the classic to-do
@@ -47,6 +51,13 @@
//! - [`hackernews`](https://github.com/gbj/leptos/tree/main/examples/hackernews) pulls everything together.
//! It integrates calls to a real external REST API, routing, server-side rendering and hydration to create
//! a fully-functional PEMPA that works as intended even before WASM has loaded and begun to run.
//! - [`hackernews-axum`](https://github.com/gbj/leptos/tree/main/examples/hackernews-axum) pulls everything together.
//! It integrates calls to a real external REST API, routing, server-side rendering and hydration to create
//! a fully-functional PEMPA that works as intended even before WASM has loaded and begun to run. This one uses Axum as it's backend.
//! - [`todo-app-sqlite`](https://github.com/gbj/leptos/tree/main/examples/todo-app-sqlite) is a simple todo app, showcasing the use of
//! functions that run only on the server, but are called from client side function calls
//! - [`todo-app-sqlite-axum`](https://github.com/gbj/leptos/tree/main/examples/todo-app-sqlite-axum) is a simple todo app, showcasing the use of
//! functions that run only on the server, but are called from client side function calls. Now with Axum backend
//!
//! (The SPA examples can be run using `trunk serve`. For information about Trunk,
//! [see here]((https://trunkrs.dev/)).)
@@ -130,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,14 +8,18 @@ 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"
[dev-dependencies]
leptos = { path = "../leptos", default-features = false, version = "0.0" }
[build-dependencies]
rustc_version = "0.4"
[features]
csr = [
"leptos/csr",

12
leptos_core/build.rs Normal file
View File

@@ -0,0 +1,12 @@
use rustc_version::{version, version_meta, Channel};
fn main() {
assert!(version().unwrap().major >= 1);
match version_meta().unwrap().channel {
Channel::Stable => {
println!("cargo:rustc-cfg=feature=\"stable\"")
}
_ => {}
}
}

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

@@ -6,10 +6,13 @@
mod for_component;
mod map;
mod suspense;
mod transition;
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,
@@ -20,7 +19,10 @@ where
/// If any [Resource](leptos_reactive::Resource)s are read in the `children` of this
/// component, it will show the `fallback` while they are loading. Once all are resolved,
/// it will render the `children`.
/// it will render the `children`. If data begin loading again, falls back to `fallback` again.
///
/// If youd rather continue displaying the previous `children` while loading new data, see
/// [`Transition`](crate::Transition).
///
/// Note that the `children` will be rendered initially (in order to capture the fact that
/// those resources are read under the suspense), so you cannot assume that resources have

View File

@@ -0,0 +1,178 @@
use leptos_dom::{Child, IntoChild};
use leptos_reactive::{provide_context, Scope, SignalSetter, 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(TypedBuilder)]
pub struct TransitionProps<F, E, G>
where
F: IntoChild + Clone,
E: IntoChild,
G: Fn() -> E,
{
/// Will be displayed while resources are pending.
pub fallback: F,
/// A function that will be called when the component transitions into or out of
/// the `pending` state, with its argument indicating whether it is pending (`true`)
/// or not pending (`false`).
#[builder(default, setter(strip_option, into))]
pub set_pending: Option<SignalSetter<bool>>,
/// Will be displayed once all resources have resolved.
pub children: Box<dyn Fn() -> Vec<G>>,
}
/// If any [Resource](leptos_reactive::Resource)s are read in the `children` of this
/// component, it will show the `fallback` while they are loading. Once all are resolved,
/// it will render the `children`. Unlike [`Suspense`](crate::Suspense), this will not fall
/// back to the `fallback` state if there are further changes after the initial load.
///
/// Note that the `children` will be rendered initially (in order to capture the fact that
/// those resources are read under the suspense), so you cannot assume that resources have
/// `Some` value in `children`.
///
/// ```
/// # use leptos_reactive::*;
/// # use leptos_core::*;
/// # use leptos_macro::*;
/// # use leptos_dom::*; use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// # if cfg!(not(any(feature = "csr", feature = "hydrate", feature = "ssr"))) {
/// async fn fetch_cats(how_many: u32) -> Result<Vec<String>, ()> { Ok(vec![]) }
///
/// let (cat_count, set_cat_count) = create_signal::<u32>(cx, 1);
/// let (pending, set_pending) = create_signal(cx, false);
///
/// let cats = create_resource(cx, cat_count, |count| fetch_cats(count));
///
/// view! { cx,
/// <div>
/// <Transition
/// fallback={"Loading...".to_string()}
/// set_pending=set_pending
/// >
/// {move || {
/// cats.read().map(|data| match data {
/// Err(_) => view! { cx, <pre>"Error"</pre> },
/// Ok(cats) => view! { cx,
/// <div>{
/// cats.iter()
/// .map(|src| {
/// view! { cx,
/// <img src={src}/>
/// }
/// })
/// .collect::<Vec<_>>()
/// }</div>
/// },
/// })
/// }
/// }
/// </Transition>
/// </div>
/// };
/// # }
/// # });
/// ```
#[allow(non_snake_case)]
pub fn Transition<F, E, G>(cx: Scope, props: TransitionProps<F, E, G>) -> impl Fn() -> Child
where
F: IntoChild + Clone,
E: IntoChild,
G: Fn() -> E + 'static,
{
let context = SuspenseContext::new(cx);
// provide this SuspenseContext to any resources below it
provide_context(cx, context);
let child = (props.children)().swap_remove(0);
render_transition(cx, context, props.fallback, child, props.set_pending)
}
#[cfg(any(feature = "csr", feature = "hydrate"))]
fn render_transition<'a, F, E, G>(
cx: Scope,
context: SuspenseContext,
fallback: F,
child: G,
set_pending: Option<SignalSetter<bool>>,
) -> impl Fn() -> Child
where
F: IntoChild + Clone,
E: IntoChild,
G: Fn() -> E,
{
use std::cell::{Cell, RefCell};
let has_rendered_once = Cell::new(false);
let prev_child = RefCell::new(Child::Null);
move || {
if context.ready() {
has_rendered_once.set(true);
let current_child = (child)().into_child(cx);
*prev_child.borrow_mut() = current_child.clone();
if let Some(pending) = &set_pending {
pending.set(false);
}
current_child
} else if has_rendered_once.get() {
if let Some(pending) = &set_pending {
pending.set(true);
}
prev_child.borrow().clone()
} else {
if let Some(pending) = &set_pending {
pending.set(true);
}
let fallback = fallback.clone().into_child(cx);
*prev_child.borrow_mut() = fallback.clone();
fallback
}
}
}
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
fn render_transition<'a, F, E, G>(
cx: Scope,
context: SuspenseContext,
fallback: F,
orig_child: G,
set_pending: Option<SignalSetter<bool>>,
) -> impl Fn() -> Child
where
F: IntoChild + Clone,
E: IntoChild,
G: Fn() -> E + 'static,
{
use leptos_dom::IntoAttribute;
use leptos_macro::view;
_ = set_pending;
let initial = {
// run the child; we'll probably throw this away, but it will register resource reads
let mut child = orig_child().into_child(cx);
while let Child::Fn(f) = child {
child = (f.borrow_mut())();
}
// no resources were read under this, so just return the child
if context.pending_resources.get() == 0 {
child
}
// show the fallback, but also prepare to stream HTML
else {
let key = cx.current_fragment_key();
cx.register_suspense(context, &key, move || {
orig_child().into_child(cx).as_child_string()
});
// return the fallback for now, wrapped in fragment identifer
Child::Node(view! { cx, <div data-fragment-id=key>{fallback.into_child(cx)}</div> })
}
};
move || initial.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"
@@ -69,6 +69,9 @@ features = [
"TransitionEvent",
]
[build-dependencies]
rustc_version = "0.4"
[dev-dependencies]
leptos = { path = "../leptos", default-features = false, version = "0.0" }
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0" }

12
leptos_dom/build.rs Normal file
View File

@@ -0,0 +1,12 @@
use rustc_version::{version, version_meta, Channel};
fn main() {
assert!(version().unwrap().major >= 1);
match version_meta().unwrap().channel {
Channel::Stable => {
println!("cargo:rustc-cfg=feature=\"stable\"")
}
_ => {}
}
}

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]
@@ -30,6 +31,9 @@ leptos = { path = "../leptos", version = "0.0", default-features = false }
leptos_router = { path = "../router", version = "0.0 " }
serde = { version = "1", features = ["derive"] }
[build-dependencies]
rustc_version = "0.4"
[features]
default = ["ssr"]
csr = ["leptos_dom/csr", "leptos_reactive/csr", "leptos/csr"]

12
leptos_macro/build.rs Normal file
View File

@@ -0,0 +1,12 @@
use rustc_version::{version, version_meta, Channel};
fn main() {
assert!(version().unwrap().major >= 1);
match version_meta().unwrap().channel {
Channel::Stable => {
println!("cargo:rustc-cfg=feature=\"stable\"")
}
_ => {}
}
}

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

@@ -46,7 +46,10 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
if #[cfg(not(feature = "stable"))] {
use proc_macro::Span;
let span = Span::call_site();
#[cfg(not(target_os = "windows"))]
let url = format!("{}/{}", span.source_file().path().to_string_lossy(), fn_name_as_str).replace("/", "-");
#[cfg(target_os = "windows")]
let url = format!("{}/{}", span.source_file().path().to_string_lossy(), fn_name_as_str).replace("\\", "-");
} else {
let url = fn_name_as_str;
}

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
@@ -1018,36 +1037,14 @@ fn create_component(cx: &Ident, node: &NodeElement, mode: Mode) -> TokenStream {
let span = node.name.span();
let component_props_name = Ident::new(&format!("{component_name}Props"), span);
let (initialize_children, children) = if node.children.is_empty() {
(quote! {}, quote! {})
let children = if node.children.is_empty() {
quote! {}
} else if node.children.len() == 1 {
let child = render_view(cx, &node.children, mode);
if mode == Mode::Hydrate {
(
quote_spanned! { span => let children = vec![#child]; },
quote_spanned! { span => .children(Box::new(move || children.clone())) },
)
} else {
(
quote! {},
quote_spanned! { span => .children(Box::new(move || vec![#child])) },
)
}
quote_spanned! { span => .children(Box::new(move || vec![#child])) }
} else {
let children = render_view(cx, &node.children, mode);
if mode == Mode::Hydrate {
(
quote_spanned! { span => let children = Box::new(move || #children); },
quote_spanned! { span => .children(children) },
)
} else {
(
quote! {},
quote_spanned! { span => .children(Box::new(move || #children)) },
)
}
quote_spanned! { span => .children(Box::new(move || #children)) }
};
let props = attributes(node).filter_map(|attr| {
@@ -1121,7 +1118,6 @@ fn create_component(cx: &Ident, node: &NodeElement, mode: Mode) -> TokenStream {
if other_attrs.peek().is_none() {
quote_spanned! {
span => create_component(#cx, move || {
#initialize_children
#component_name(
#cx,
#component_props_name::builder()
@@ -1134,7 +1130,6 @@ fn create_component(cx: &Ident, node: &NodeElement, mode: Mode) -> TokenStream {
} else {
quote_spanned! {
span => create_component(#cx, move || {
#initialize_children
let #component_name = #component_name(
#cx,
#component_props_name::builder()

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"
@@ -45,6 +45,9 @@ serde = []
serde-lite = ["dep:serde-lite"]
miniserde = ["dep:miniserde"]
[build-dependencies]
rustc_version = "0.4"
[package.metadata.cargo-all-features]
denylist = ["stable"]
skip_feature_sets = [

12
leptos_reactive/build.rs Normal file
View File

@@ -0,0 +1,12 @@
use rustc_version::{version, version_meta, Channel};
fn main() {
assert!(version().unwrap().major >= 1);
match version_meta().unwrap().channel {
Channel::Stable => {
println!("cargo:rustc-cfg=feature=\"stable\"")
}
_ => {}
}
}

View File

@@ -77,6 +77,7 @@ mod selector;
mod serialization;
mod signal;
mod signal_wrappers_read;
mod signal_wrappers_write;
mod spawn;
mod suspense;
@@ -91,6 +92,7 @@ pub use selector::*;
pub use serialization::*;
pub use signal::*;
pub use signal_wrappers_read::*;
pub use signal_wrappers_write::*;
pub use spawn::*;
pub use suspense::*;

View File

@@ -1,6 +1,6 @@
use std::rc::Rc;
use crate::{Memo, ReadSignal, RwSignal, Scope, UntrackedGettableSignal};
use crate::{RwSignal, Scope, WriteSignal};
/// A wrapper for any kind of settable reactive signal: a [WriteSignal](crate::WriteSignal),
/// [RwSignal](crate::RwSignal), or closure that receives a value and sets a signal depending
@@ -14,13 +14,13 @@ use crate::{Memo, ReadSignal, RwSignal, Scope, UntrackedGettableSignal};
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 2);
/// let set_double_input = SignalSetter::map(cx, |n| set_count(n * 2));
/// let set_double_input = SignalSetter::map(cx, move |n| set_count(n * 2));
///
/// // this function takes any kind of signal setter
/// fn set_to_4(setter: &SignalSetter<i32>) -> bool {
/// fn set_to_4(setter: &SignalSetter<i32>) {
/// // ✅ calling the signal sets the value
/// // it is a shorthand for arg.set()
/// setter(4)
/// setter(4);
/// }
///
/// set_to_4(&set_count.into());
@@ -44,10 +44,10 @@ where
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 2);
/// let double_count = SignalSetter::map(cx, move |n| set_count(n * 2));
/// let set_double_count = SignalSetter::map(cx, move |n| set_count(n * 2));
///
/// // this function takes any kind of signal setter
/// fn set_to_4(setter: &SignalSetter<i32>) -> bool {
/// fn set_to_4(setter: &SignalSetter<i32>) {
/// // ✅ calling the signal sets the value
/// // it is a shorthand for arg.set()
/// setter(4)
@@ -55,7 +55,7 @@ where
///
/// set_to_4(&set_count.into());
/// assert_eq!(count(), 4);
/// set_to_4(&set_double_input);
/// set_to_4(&set_double_count);
/// assert_eq!(count(), 8);
/// # });
/// ```
@@ -69,24 +69,24 @@ where
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 2);
/// let double_count = SignalSetter::map(cx, move |n| set_count(n * 2));
/// let set_double_count = SignalSetter::map(cx, move |n| set_count(n * 2));
///
/// // this function takes any kind of signal setter
/// fn set_to_4(setter: &SignalSetter<i32>) -> bool {
/// fn set_to_4(setter: &SignalSetter<i32>) {
/// // ✅ calling the signal sets the value
/// // it is a shorthand for arg.set()
/// setter(4)
/// setter(4);
/// }
///
/// set_to_4(&set_count.into());
/// assert_eq!(count(), 4);
/// set_to_4(&set_double_input);
/// set_to_4(&set_double_count);
/// assert_eq!(count(), 8);
/// # });
pub fn set(&self, value: T) {
match &self.0 {
SignalSetterTypes::Write(s) => s.set(value),
SignalSetterTypes::Wrapped(_, s) => s(value),
SignalSetterTypes::Mapped(_, s) => s(value),
}
}
}
@@ -108,8 +108,8 @@ enum SignalSetterTypes<T>
where
T: 'static,
{
Write(WriteSignalSetter<T>),
Mapped(Scope, Rc<dyn FnOnce(T)>),
Write(WriteSignal<T>),
Mapped(Scope, Rc<dyn Fn(T)>),
}
impl<T> std::fmt::Debug for SignalSetterTypes<T>
@@ -118,9 +118,8 @@ where
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ReadSignal(arg0) => f.debug_tuple("ReadSignal").field(arg0).finish(),
Self::Memo(arg0) => f.debug_tuple("Memo").field(arg0).finish(),
Self::DerivedSignal(_, _) => f.debug_tuple("DerivedSignal").finish(),
Self::Write(arg0) => f.debug_tuple("WriteSignal").field(arg0).finish(),
Self::Mapped(_, _) => f.debug_tuple("Mapped").finish(),
}
}
}
@@ -131,9 +130,8 @@ where
{
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::ReadSignal(l0), Self::ReadSignal(r0)) => l0 == r0,
(Self::Memo(l0), Self::Memo(r0)) => l0 == r0,
(Self::DerivedSignal(_, l0), Self::DerivedSignal(_, r0)) => std::ptr::eq(l0, r0),
(Self::Write(l0), Self::Write(r0)) => l0 == r0,
(Self::Mapped(_, l0), Self::Mapped(_, r0)) => std::ptr::eq(l0, r0),
_ => false,
}
}

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.3"
version = "0.0.5"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@@ -14,10 +14,14 @@ typed-builder = "0.11"
[dependencies.web-sys]
version = "0.3"
features = ["HtmlLinkElement", "HtmlTitleElement"]
features = ["HtmlLinkElement", "HtmlMetaElement", "HtmlTitleElement"]
[features]
default = ["csr"]
csr = ["leptos/csr"]
hydrate = ["leptos/hydrate"]
ssr = ["leptos/ssr"]
stable = ["leptos/stable"]
[package.metadata.cargo-all-features]
denylist = ["stable"]

View File

@@ -36,12 +36,14 @@
//!
//! ```
use std::fmt::Debug;
use std::{fmt::Debug, rc::Rc};
use leptos::{leptos_dom::debug_warn, *};
mod meta_tags;
mod stylesheet;
mod title;
pub use meta_tags::*;
pub use stylesheet::*;
pub use title::*;
@@ -53,6 +55,7 @@ pub use title::*;
pub struct MetaContext {
pub(crate) title: TitleContext,
pub(crate) stylesheets: StylesheetContext,
pub(crate) meta_tags: MetaTagsContext
}
/// Returns the current [MetaContext].
@@ -123,13 +126,23 @@ impl MetaContext {
// Stylesheets
tags.push_str(&self.stylesheets.as_string());
// Meta tags
tags.push_str(&self.meta_tags.as_string());
tags
}
}
/// Describes a value that is either a static or a reactive string, i.e.,
/// a [String], a [&str], or a reactive `Fn() -> String`.
pub struct TextProp(Box<dyn Fn() -> String>);
#[derive(Clone)]
pub struct TextProp(Rc<dyn Fn() -> String>);
impl TextProp {
fn get(&self) -> String {
(self.0)()
}
}
impl Debug for TextProp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@@ -139,14 +152,14 @@ impl Debug for TextProp {
impl From<String> for TextProp {
fn from(s: String) -> Self {
TextProp(Box::new(move || s.clone()))
TextProp(Rc::new(move || s.clone()))
}
}
impl From<&str> for TextProp {
fn from(s: &str) -> Self {
let s = s.to_string();
TextProp(Box::new(move || s.clone()))
TextProp(Rc::new(move || s.clone()))
}
}
@@ -155,6 +168,6 @@ where
F: Fn() -> String + 'static,
{
fn from(s: F) -> Self {
TextProp(Box::new(s))
TextProp(Rc::new(s))
}
}

187
meta/src/meta_tags.rs Normal file
View File

@@ -0,0 +1,187 @@
use cfg_if::cfg_if;
use leptos::Scope;
use std::{rc::Rc, cell::{RefCell, Cell}, collections::HashMap};
use typed_builder::TypedBuilder;
use crate::{use_head, TextProp};
/// Manages all of the `<meta>` elements set by [Meta] components.
#[derive(Clone, Default, Debug)]
pub struct MetaTagsContext {
next_id: Cell<MetaTagId>,
#[allow(clippy::type_complexity)]
els: Rc<RefCell<HashMap<MetaTagId, (Option<MetaTag>, Option<web_sys::HtmlMetaElement>)>>>,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
struct MetaTagId(usize);
impl MetaTagsContext {
fn get_next_id(&self) -> MetaTagId {
let current_id = self.next_id.get();
let next_id = MetaTagId(current_id.0 + 1);
self.next_id.set(next_id);
next_id
}
}
#[derive(Clone, Debug)]
enum MetaTag {
Charset(TextProp),
HttpEquiv {
http_equiv: TextProp,
content: Option<TextProp>
},
Name {
name: TextProp,
content: TextProp
}
}
impl MetaTagsContext {
/// Converts the set of `<meta>` elements into an HTML string that can be injected into the `<head>`.
pub fn as_string(&self) -> String {
self.els
.borrow()
.iter()
.filter_map(|(id, (tag, _))| {
tag.as_ref().map(|tag| {
let id = id.0;
match tag {
MetaTag::Charset(charset) => format!(r#"<meta charset="{}" data-leptos-meta="{id}">"#, charset.get()),
MetaTag::HttpEquiv { http_equiv, content } => {
if let Some(content) = &content {
format!(r#"<meta http-equiv="{}" content="{}" data-leptos-meta="{id}">"#, http_equiv.get(), content.get())
} else {
format!(r#"<meta http-equiv="{}" data-leptos-meta="{id}">"#, http_equiv.get())
}
},
MetaTag::Name { name, content } => format!(r#"<meta name="{}" content="{}" data-leptos-meta="{id}">"#, name.get(), content.get()),
}
})
})
.collect()
}
}
/// Properties for the [Meta] component.
#[derive(TypedBuilder)]
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))]
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))]
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))]
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))]
pub content: Option<TextProp>,
}
/// Injects an [HTMLMetaElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMetaElement) into the document
/// head to set metadata
///
/// ```
/// use leptos::*;
/// use leptos_meta::*;
///
/// #[component]
/// fn MyApp(cx: Scope) -> Element {
/// provide_context(cx, MetaContext::new());
///
/// view! { cx,
/// <main>
/// <Meta charset="utf-8"/>
/// <Meta name="description" content="A Leptos fan site."/>
/// <Meta http_equiv="refresh" content="3;url=https://github.com/gbj/leptos"/>
/// </main>
/// }
/// }
/// ```
#[allow(non_snake_case)]
pub fn Meta(cx: Scope, props: MetaProps) {
let MetaProps { charset, name, http_equiv, content } = props;
let tag = match (charset, name, http_equiv, content) {
(Some(charset), _, _, _) => MetaTag::Charset(charset),
(_, _, Some(http_equiv), content) => MetaTag::HttpEquiv { http_equiv, content },
(_, Some(name), _, Some(content)) => MetaTag::Name { name, content },
_ => panic!("<Meta/> tag expects either `charset`, `http_equiv`, or `name` and `content` to be set.")
};
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
use leptos::{document, JsCast, UnwrapThrowExt, create_element, create_effect, set_attribute};
let meta = use_head(cx);
let meta_tags = meta.meta_tags;
let id = meta_tags.get_next_id();
let el = if let Ok(Some(el)) = document().query_selector(&format!("[data-leptos-meta={}]", id.0)) {
el
} else {
create_element("meta")
};
match tag {
MetaTag::Charset(charset) => {
create_effect(cx, {
let el = el.clone();
move |_| {
set_attribute(&el, "charset", &charset.get());
}
})
},
MetaTag::HttpEquiv { http_equiv, content } => {
create_effect(cx, {
let el = el.clone();
move |_| {
set_attribute(&el, "http-equiv", &http_equiv.get());
}
});
if let Some(content) = content {
create_effect(cx, {
let el = el.clone();
move |_| {
set_attribute(&el, "content", &content.get());
}
});
}
},
MetaTag::Name { name, content } => {
create_effect(cx, {
let el = el.clone();
move |_| {
set_attribute(&el, "name", &name.get());
}
});
create_effect(cx, {
let el = el.clone();
move |_| {
set_attribute(&el, "content", &content.get());
}
});
},
}
// add to head
document()
.query_selector("head")
.unwrap_throw()
.unwrap_throw()
.append_child(&el)
.unwrap_throw();
// add to meta tags
meta_tags.els.borrow_mut().insert(id, (None, Some(el.unchecked_into())));
} else {
let meta = use_head(cx);
let meta_tags = meta.meta_tags;
meta_tags.els.borrow_mut().insert(meta_tags.get_next_id(), (Some(tag), None));
}
}
}

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