Compare commits

..

82 Commits

Author SHA1 Message Date
Greg Johnston
1c8b640855 Update #[component] docs 2022-12-14 06:44:14 -05:00
Greg Johnston
621976c92c Add correct import for doctest 2022-12-13 14:14:04 -05:00
Greg Johnston
b2d7ad2afd Fix a couple issues with intra-doc links 2022-12-13 13:10:04 -05:00
Greg Johnston
73b21487b9 Add more entry-level docs for #[component] macro 2022-12-13 13:06:37 -05:00
Greg Johnston
0b448daf3a Fix SimpleCounter example in tests 2022-12-09 14:58:53 -05:00
Greg Johnston
c01dba5138 Merge pull request #160 from gbj/component-documentation
Allows documenting `Component` and `ComponentProps` in a single doc comment
2022-12-09 14:27:36 -05:00
Greg Johnston
1929f2d8b2 Merge pull request #161 from gbj/docs-improvements
Docs improvements
2022-12-09 13:56:18 -05:00
Greg Johnston
50b0fe157a Fix example test 2022-12-09 13:34:35 -05:00
Greg Johnston
dc7f44933c Add cargo-leptos to Readme 2022-12-09 13:28:26 -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
b56dde9a6d Add working Tailwind example per issue #147 2022-12-09 13:05:20 -05:00
Greg Johnston
74ec8925dc Additional documentation for issue #156 2022-12-09 12:41:17 -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
3d10bbb0c6 Merge pull request #159 from benwis/dashes
Replace _ with - for KDL files
2022-12-08 13:15:21 -05:00
Ben Wishovich
8d325fce5c Replace _ with - for KDL files 2022-12-08 10:08:04 -08:00
Greg Johnston
7e457ee202 Merge pull request #157 from akesson/integration-html-updates
Integration html updates
2022-12-08 08:05:25 -05:00
hakesson
bb282189c3 Add preload of js and wasm 2022-12-08 08:11:15 +01:00
hakesson
2694d2e93c Add missing init param 2022-12-08 08:10:56 +01: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
113 changed files with 2862 additions and 1971 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

@@ -54,6 +54,17 @@ Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained re
- **Fine-grained reactivity**: The entire framework is build from reactive primitives. This allows for extremely performant code with minimal overhead: when a reactive signals value changes, it can update a single text node, toggle a single class, or remove an element from the DOM without any other code running. (_So, no virtual DOM!_)
- **Declarative**: Tell Leptos how you want the page to look, and let the framework tell the browser how to do it.
## Getting Started
The best way to get started with a Leptos project right now is to use the [`cargo-leptos`](https://github.com/akesson/cargo-leptos) build tool and our [starter template](https://github.com/leptos-rs/start).
```bash
cargo install cargo-leptos
cargo leptos new --git https://github.com/leptos-rs/start
cd [your project name]
cargo leptos watch
```
## Learn more
Here are some resources for learning more about Leptos:

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 {
/// 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, 0);
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.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=3 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>

10
examples/tailwind/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk

View File

@@ -0,0 +1,92 @@
[workspace]
[package]
name = "example"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
leptos = { git = "https://github.com/gbj/leptos", default-features = false, features = [
"serde",
] }
leptos_meta = { git = "https://github.com/gbj/leptos", default-features = false }
leptos_router = { git = "https://github.com/gbj/leptos", default-features = false }
gloo-net = { version = "0.2", features = ["http"] }
log = "0.4"
cfg-if = "1.0"
# dependecies for client (enable when csr or hydrate set)
wasm-bindgen = { version = "0.2", optional = true }
console_log = { version = "0.2", optional = true }
console_error_panic_hook = { version = "0.1", optional = true }
# dependecies for server (enable when ssr set)
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", features = ["macros"], optional = true }
futures = { version = "0.3", optional = true }
simple_logger = { version = "4.0", optional = true }
serde_json = { version = "1.0", optional = true }
reqwest = { version = "0.11", features = ["json"], optional = true }
[profile.release]
codegen-units = 1
lto = true
opt-level = 'z'
[features]
leptos_autoreload = []
default = ["csr"]
hydrate = [
"leptos/hydrate",
"leptos_meta/hydrate",
"leptos_router/hydrate",
"dep:wasm-bindgen",
"dep:console_log",
"dep:console_error_panic_hook",
]
csr = [
"leptos/csr",
"leptos_meta/csr",
"leptos_router/csr",
"dep:wasm-bindgen",
"dep:console_log",
"dep:console_error_panic_hook",
]
ssr = [
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:reqwest",
"dep:actix-web",
"dep:actix-files",
"dep:futures",
"dep:simple_logger",
"dep:serde_json",
]
[package.metadata.leptos]
# Path, relative to root, to generat rust code to
gen_file = "src/server/generated.rs"
# Path to the source index.html file
index_file = "index.html"
# [Optional] Files in the asset_dir will be copied to the target/site directory
assets_dir = "assets"
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
end2end_test_cmd = "npx playwright test"
# On which port to serve the client side rendered site (when using --csr option)
csr_port = 3000
# The port to use for automatic reload monitoring
reload_port = 3001
[package.metadata.leptos.style]
# This points to the TailwindCSS output file
file = "style/output.css"
# A https://browsersl.ist query
browserquery = "defaults"

21
examples/tailwind/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 henrik
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,74 @@
# Leptos Starter Template
This is a template demonstrating how to integrate [TailwindCSS](https://tailwindcss.com/) with the [Leptos](https://github.com/gbj/leptos) web framework and the [cargo-leptos](https://github.com/akesson/cargo-leptos) tool.
If you don't have `cargo-leptos` installed you can install it with
`cargo install --locked cargo-leptos`
Then run
`npx tailwindcss -i ./input.css -o ./style/output.scss --watch`
and
`cargo leptos watch`
in this directory.
You can begin editing your app at `src/app/mod.rs`.
## Installing Tailwind
You can install Tailwind using `npm`:
```bash
npm install -D tailwindcss
```
If you'd rather not use `npm`, you can install the Tailwind binary [here](https://github.com/tailwindlabs/tailwindcss/releases).
## Setting up with VS Code and Additional Tools
If you're using VS Code, add the following to your `settings.json`
```json
"emmet.includeLanguages": {
"rust": "html",
"*.rs": "html"
},
"tailwindCSS.includeLanguages": {
"rust": "html",
"*.rs": "html"
},
"files.associations": {
"*.rs": "rust"
},
"editor.quickSuggestions": {
"other": "on",
"comments": "on",
"strings": true
},
"css.validate": false,
```
Install [Tailwind CSS Intellisense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss).
Install "VS Browser" extension, a browser at the right window.
Allow vscode Ports forward: 3000, 3001.
## Notes about Tooling
By default, `cargo-leptos` uses `nightly` Rust, `cargo-generate`, and `sass`. If you run into any trouble, you may need to install one or more of these tools.
1. `rustup toolchain install nightly --allow-downgrade` - make sure you have Rust nightly
2. `rustup default nightly` - setup nightly as default, or you can use rust-toolchain file later on
3. `rustup target add wasm32-unknown-unknown` - add the ability to compile Rust to WebAssembly
4. `cargo install cargo-generate` - install `cargo-generate` binary (should be installed automatically in future)
5. `npm install -g sass` - install `dart-sass` (should be optional in future
## Attribution
Many thanks to GreatGreg for putting together this guide. You can find the original, with added details, [here](https://github.com/gbj/leptos/discussions/125).

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

View File

@@ -0,0 +1,74 @@
{
"name": "end2end",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "end2end",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.28.0"
}
},
"node_modules/@playwright/test": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz",
"integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==",
"dev": true,
"dependencies": {
"@types/node": "*",
"playwright-core": "1.28.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@types/node": {
"version": "18.11.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
"dev": true
},
"node_modules/playwright-core": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz",
"integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==",
"dev": true,
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=14"
}
}
},
"dependencies": {
"@playwright/test": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz",
"integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==",
"dev": true,
"requires": {
"@types/node": "*",
"playwright-core": "1.28.0"
}
},
"@types/node": {
"version": "18.11.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
"dev": true
},
"playwright-core": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz",
"integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==",
"dev": true
}
}
}

View File

@@ -0,0 +1,13 @@
{
"name": "end2end",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.28.0"
}
}

View File

@@ -0,0 +1,107 @@
import type { PlaywrightTestConfig } from "@playwright/test";
import { devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
const config: PlaywrightTestConfig = {
testDir: "./tests",
/* Maximum time one test can run for. */
timeout: 30 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 5000,
},
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
},
},
{
name: "firefox",
use: {
...devices["Desktop Firefox"],
},
},
{
name: "webkit",
use: {
...devices["Desktop Safari"],
},
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: {
// ...devices['Pixel 5'],
// },
// },
// {
// name: 'Mobile Safari',
// use: {
// ...devices['iPhone 12'],
// },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: {
// channel: 'msedge',
// },
// },
// {
// name: 'Google Chrome',
// use: {
// channel: 'chrome',
// },
// },
],
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
// outputDir: 'test-results/',
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// port: 3000,
// },
};
export default config;

View File

@@ -0,0 +1,9 @@
import { test, expect } from "@playwright/test";
test("homepage has title and links to intro page", async ({ page }) => {
await page.goto("http://localhost:3000/");
await expect(page).toHaveTitle("Cargo Leptos");
await expect(page.locator("h1")).toHaveText("Hi from your Leptos WASM!");
});

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Cargo Leptos</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- INJECT HEAD -->
</head>
<body>
<!-- INJECT BODY -->
</body>
</html>

View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,26 @@
use leptos::*;
use leptos_meta::*;
#[component]
pub fn App(cx: Scope) -> Element {
provide_context(cx, MetaContext::default());
let (count, set_count) = create_signal(cx, 0);
view! {
cx,
<main class="my-0 mx-auto max-w-3xl text-center">
<h2 class="p-6 text-4xl">"Welcome to Leptos with Tailwind"</h2>
<p class="px-10 pb-10 text-left">"Tailwind will scan your Rust files for Tailwind class names and compile them into a CSS file."</p>
<button
class="bg-sky-600 hover:bg-sky-700 px-5 py-3 text-white rounded-lg"
on:click=move |_| set_count.update(|count| *count += 1)
>
{move || if count() == 0 {
"Click me!".to_string()
} else {
count().to_string()
}}
</button>
</main>
}
}

View File

@@ -0,0 +1,42 @@
mod app;
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen(start)]
pub fn main() {
use app::*;
use leptos::*;
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
log!("hydrate mode - hydrating");
leptos::hydrate(body().unwrap(), move |cx| {
view! { cx, <App/> }
});
}
}
else if #[cfg(feature = "csr")] {
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen(start)]
pub fn main() {
use app::*;
use leptos::*;
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
log!("csr mode - mounting to body");
mount_to_body(|cx| {
view! { cx, <App /> }
});
}
}
}

View File

@@ -0,0 +1,17 @@
mod app;
#[cfg(feature = "ssr")]
mod server;
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
#[actix_web::main]
async fn main() -> std::io::Result<()> {
server::run().await
}
}
else {
pub fn main() {}
}
}

View File

@@ -0,0 +1,49 @@
//! THIS FILE IS AUTOGENERATED, DO NOT MODIFY
//! When building, `cargo-leptos` generates this file based on
//! the `index.html` file specified in the Config.toml
//!
//! This file can be commited to version control. It only
//! changes when the configuration changes
#[cfg(feature = "leptos_autoreload")]
/// index.html content up to `<!-- INJECT HEAD -->` plus `cargo leptos` injected css and js content.
pub const HTML_START: &str = r##"<!DOCTYPE html>
<html lang="en">
<head>
<title>Cargo Leptos</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script type="module">import init from '/pkg/app.js';init('/pkg/app.wasm');</script>
<link rel="preload" href="/pkg/app.wasm" as="fetch" type="application/wasm" crossorigin="">
<link rel="stylesheet" href="/pkg/app.css">
<link rel="modulepreload" href="/pkg/app.js">
<script crossorigin="">(function () {
var ws = new WebSocket('ws://127.0.0.1:3001/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>"##;
#[cfg(not(feature = "leptos_autoreload"))]
/// index.html content up to `<!-- INJECT HEAD -->` plus `cargo leptos` injected css and js content.
pub const HTML_START: &str = r##"<!DOCTYPE html>
<html lang="en">
<head>
<title>Cargo Leptos</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script type="module">import init from '/pkg/app.js';init('/pkg/app.wasm');</script>
<link rel="preload" href="/pkg/app.wasm" as="fetch" type="application/wasm" crossorigin="">
<link rel="stylesheet" href="/pkg/app.css">
<link rel="modulepreload" href="/pkg/app.js">"##;
/// index.html content from `<!-- INJECT HEAD -->` up to `<!-- INJECT BODY -->`
pub const HTML_MIDDLE: &str = r##" </head>
<body>"##;
/// index.html content from `<!-- INJECT BODY -->` until the end
pub const HTML_END: &str = r##" </body>
</html>"##;

View File

@@ -0,0 +1,92 @@
mod generated;
use crate::app::*;
use actix_files::Files;
use actix_web::*;
use futures::StreamExt;
use generated::{HTML_END, HTML_MIDDLE, HTML_START};
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
#[derive(Copy, Clone, Debug)]
struct ActixIntegration {
path: ReadSignal<String>,
}
impl History for ActixIntegration {
fn location(&self, cx: leptos::Scope) -> ReadSignal<LocationChange> {
create_signal(
cx,
LocationChange {
value: self.path.get(),
replace: false,
scroll: true,
state: State(None),
},
)
.0
}
fn navigate(&self, _loc: &LocationChange) {}
}
// 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 = ActixIntegration {
path: create_signal(cx, path.clone()).0,
};
provide_context(cx, RouterIntegrationContext(std::rc::Rc::new(integration)));
view! { cx, <App /> }
};
HttpResponse::Ok().content_type("text/html").streaming(
futures::stream::once(async { HTML_START.to_string() })
.chain(render_to_stream(move |cx| {
use_context::<MetaContext>(cx)
.map(|meta| meta.dehydrate())
.unwrap_or_default()
}))
.chain(futures::stream::once(async { HTML_MIDDLE.to_string() }))
.chain(render_to_stream(move |cx| app(cx).to_string()))
.chain(futures::stream::once(async { HTML_END.to_string() }))
.map(|html| Ok(web::Bytes::from(html)) as Result<web::Bytes>),
)
}
pub async fn run() -> 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(|_| "3000".to_string())
.parse::<u16>()
.unwrap();
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
log::info!("serving at {host}:{port}");
HttpServer::new(|| {
App::new()
.service(
web::scope("/pkg")
.service(Files::new("", "target/site/pkg"))
.wrap(middleware::Compress::default()),
)
.service(render_app)
})
.bind((host, port))?
.run()
.await
}

View File

@@ -0,0 +1,2 @@
/** Imports your Tailwind output */
@import './output.scss';

View File

@@ -0,0 +1,583 @@
/*
! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com
*/
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/
*,
::before,
::after {
box-sizing: border-box;
/* 1 */
border-width: 0;
/* 2 */
border-style: solid;
/* 2 */
border-color: #e5e7eb;
/* 2 */
}
::before,
::after {
--tw-content: '';
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
*/
html {
line-height: 1.5;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
-moz-tab-size: 4;
/* 3 */
-o-tab-size: 4;
tab-size: 4;
/* 3 */
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* 4 */
font-feature-settings: normal;
/* 5 */
}
/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
body {
margin: 0;
/* 1 */
line-height: inherit;
/* 2 */
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/
hr {
height: 0;
/* 1 */
color: inherit;
/* 2 */
border-top-width: 1px;
/* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font family by default.
2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0;
/* 1 */
border-color: inherit;
/* 2 */
border-collapse: collapse;
/* 3 */
}
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-size: 100%;
/* 1 */
font-weight: inherit;
/* 1 */
line-height: inherit;
/* 1 */
color: inherit;
/* 1 */
margin: 0;
/* 2 */
padding: 0;
/* 3 */
}
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
text-transform: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
[type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button;
/* 1 */
background-color: transparent;
/* 2 */
background-image: none;
/* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Removes the default spacing and border for appropriate elements.
*/
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
fieldset {
margin: 0;
padding: 0;
}
legend {
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/
input::-moz-placeholder, textarea::-moz-placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
input::placeholder,
textarea::placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role="button"] {
cursor: pointer;
}
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
/* 1 */
vertical-align: middle;
/* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden] {
display: none;
}
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
.my-0 {
margin-top: 0px;
margin-bottom: 0px;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.max-w-3xl {
max-width: 48rem;
}
.rounded-lg {
border-radius: 0.5rem;
}
.bg-sky-600 {
--tw-bg-opacity: 1;
background-color: rgb(2 132 199 / var(--tw-bg-opacity));
}
.p-6 {
padding: 1.5rem;
}
.px-10 {
padding-left: 2.5rem;
padding-right: 2.5rem;
}
.px-5 {
padding-left: 1.25rem;
padding-right: 1.25rem;
}
.py-3 {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
}
.pb-10 {
padding-bottom: 2.5rem;
}
.text-left {
text-align: left;
}
.text-center {
text-align: center;
}
.text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.hover\:bg-sky-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(3 105 161 / var(--tw-bg-opacity));
}

View File

@@ -0,0 +1,10 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: {
files: ["*.html", "./src/**/*.rs"],
},
theme: {
extend: {},
},
plugins: [],
}

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,40 @@ 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>"#);
<link rel="modulepreload" href="{pkg_path}.js">
<link rel="preload" href="{pkg_path}.wasm" as="fetch" type="application/wasm" crossorigin="">
<script type="module">import init, {{ hydrate }} from '{pkg_path}.js'; init('{pkg_path}.wasm').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,38 @@ 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>"#
<link rel="modulepreload" href="{pkg_path}.js">
<link rel="preload" href="{pkg_path}.wasm" as="fetch" type="application/wasm" crossorigin="">
<script type="module">import init, {{ hydrate }} from '{pkg_path}.js'; init('{pkg_path}.wasm').then(hydrate);</script>
{leptos_autoreload}
"#
);
let tail = "</body></html>";
@@ -219,9 +256,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
@@ -179,7 +177,8 @@ mod server;
/// # });
/// ```
///
/// 8. You can use the `_ref` attribute to store a reference to its DOM element in a [NodeRef](leptos::NodeRef) to use later.
/// 8. You can use the `_ref` attribute to store a reference to its DOM element in a
/// [NodeRef](leptos_reactive::NodeRef) to use later.
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # run_scope(create_runtime(), |cx| {
@@ -244,9 +243,50 @@ pub fn view(tokens: TokenStream) -> TokenStream {
}
}
/// Annotates a function so that it can be used with your template as a <Component/>
/// Annotates a function so that it can be used with your template as a Leptos `<Component/>`.
///
/// The `#[component]` macro allows you to annotate plain Rust functions that return [Element](leptos_dom::Element)s,
/// and use them within your Leptos [view](mod@view) as if they were custom HTML elements. The
/// component function takes a [Scope](leptos_reactive::Scope) and any number of other arguments.
/// When you use the component somewhere else, the names of its arguments are the names
/// of the properties you use in the [view](mod@view) macro.
///
/// Heres how you would define and use a simple Leptos component which can accept custom properties for a name and age:
/// ```rust
/// # use leptos::*;
/// use std::time::Duration;
///
/// #[component]
/// fn HelloComponent(cx: Scope, name: String, age: u8) -> Element {
/// // create the signals (reactive values) that will update the UI
/// let (age, set_age) = create_signal(cx, age);
/// // increase `age` by 1 every second
/// set_interval(move || {
/// set_age.update(|age| *age += 1)
/// }, Duration::from_secs(1));
///
/// // return the user interface, which will be automatically updated
/// // when signal values change
/// view! { cx,
/// <p>"Your name is " {name} " and you are " {age} " years old."</p>
/// }
/// }
///
/// #[component]
/// fn App(cx: Scope) -> Element {
/// view! { cx,
/// <main>
/// <HelloComponent name="Greg".to_string() age=32/>
/// </main>
/// }
/// }
/// ```
///
/// The `#[component]` macro creates a struct with a name like `HelloComponentProps`. If you define
/// your component in one module and import it into another, make sure you import this `___Props`
/// struct as well.
///
/// Here are some things you should know.
/// Here are some important details about how Leptos components work within the framework:
/// 1. **The component function only runs once.** Your component function is not a “render” function
/// that re-runs whenever changes happen in the state. Its a “setup” function that runs once to
/// create the user interface, and sets up a reactive system to update it. This means its okay
@@ -352,7 +392,7 @@ pub fn component(_args: proc_macro::TokenStream, s: TokenStream) -> TokenStream
}
}
/// Declares that a function is a [server function](leptos::leptos_server). This means that
/// Declares that a function is a [server function](leptos_server). This means that
/// its body will only run on the server, i.e., when the `ssr` feature is enabled.
///
/// If you call a server function from the client (i.e., when the `csr` or `hydrate` features
@@ -408,15 +448,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"]

Some files were not shown because too many files have changed in this diff Show More