Compare commits

...

51 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
74 changed files with 2082 additions and 1684 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

@@ -9,6 +9,7 @@ cfg_if! {
use actix_files::{Files};
use actix_web::*;
use crate::counters::*;
use std::{net::SocketAddr, env};
#[get("/api/events")]
async fn counter_events() -> impl Responder {
@@ -29,17 +30,20 @@ cfg_if! {
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let addr = SocketAddr::from(([127,0,0,1],3000));
crate::counters::register_server_functions();
HttpServer::new(|| {
HttpServer::new(move || {
let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/leptos_counter_isomorphic").reload_port(3001).socket_address(addr.clone()).environment(&env::var("RUST_ENV")).build();
render_options.write_to_file();
App::new()
.service(Files::new("/pkg", "./pkg"))
.service(counter_events)
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.route("/{tail:.*}", leptos_actix::render_app_to_stream("leptos_counter_isomorphic", |cx| view! { cx, <Counters/> }))
.route("/{tail:.*}", leptos_actix::render_app_to_stream(render_options, |cx| view! { cx, <Counters/> }))
//.wrap(middleware::Compress::default())
})
.bind(("127.0.0.1", 8081))?
.bind(&addr)?
.run()
.await
}

View File

@@ -1,14 +1,22 @@
use leptos::*;
pub fn simple_counter(cx: Scope) -> web_sys::Element {
/// 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

@@ -12,11 +12,12 @@ if #[cfg(feature = "ssr")] {
use http::StatusCode;
use std::net::SocketAddr;
use tower_http::services::ServeDir;
use std::env;
#[tokio::main]
async fn main() {
use leptos_hackernews_axum::*;
let addr = SocketAddr::from(([127, 0, 0, 1], 8082));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
log::debug!("serving at {addr}");
@@ -36,12 +37,14 @@ if #[cfg(feature = "ssr")] {
)
}
let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/leptos_hackernews_axum").socket_address(addr).reload_port(3001).environment(&env::var("RUST_ENV")).build();
render_options.write_to_file();
// build our application with a route
let app = Router::new()
// `GET /` goes to `root`
.nest_service("/pkg", pkg_service)
.nest_service("/static", static_service)
.fallback(leptos_axum::render_app_to_stream("leptos_hackernews_axum", |cx| view! { cx, <App/> }));
.fallback(leptos_axum::render_app_to_stream(render_options, |cx| view! { cx, <App/> }));
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`

View File

@@ -14,11 +14,10 @@ console_log = "0.2"
console_error_panic_hook = "0.1"
futures = "0.3"
cfg-if = "1"
leptos = { path = "../../leptos", default-features = false, features = [
"serde",
] }
leptos_meta = { path = "../../meta", default-features = false }
leptos_router = { path = "../../router", default-features = false }
leptos = { version = "0.0.20", default-features = false, features = ["serde"] }
leptos_meta = { version = "0.0", default-features = false }
leptos_actix = { version = "0.0.2", default-features = false, optional = true }
leptos_router = { version = "0.0", default-features = false }
log = "0.4"
simple_logger = "2"
serde = { version = "1", features = ["derive"] }
@@ -35,11 +34,12 @@ hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:actix-files",
"dep:actix-web",
"dep:leptos_actix",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
]
[package.metadata.cargo-all-features]
denylist = ["actix-files", "actix-web"]
denylist = ["actix-files", "actix-web", "leptos_actix"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]

View File

@@ -16,7 +16,7 @@ pub fn App(cx: Scope) -> Element {
view! {
cx,
<div>
<Stylesheet href="/static/style.css"/>
<Stylesheet href="/style.css"/>
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
<Router>
<Nav />
@@ -38,7 +38,7 @@ cfg_if! {
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn main() {
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::hydrate(body().unwrap(), move |cx| {

View File

@@ -3,109 +3,39 @@ use leptos::*;
// boilerplate to run in different modes
cfg_if! {
// server-only stuff
if #[cfg(feature = "ssr")] {
use actix_files::{Files, NamedFile};
use actix_files::{Files};
use actix_web::*;
use futures::StreamExt;
use leptos_meta::*;
use leptos_router::*;
use leptos_hackernews::*;
use std::{net::SocketAddr, env};
#[get("/static/style.css")]
#[get("/style.css")]
async fn css() -> impl Responder {
NamedFile::open_async("./style.css").await
}
// match every path — our router will handle actual dispatch
#[get("{tail:.*}")]
async fn render_app(req: HttpRequest) -> impl Responder {
let path = req.path();
let query = req.query_string();
let path = if query.is_empty() {
"http://leptos".to_string() + path
} else {
"http://leptos".to_string() + path + "?" + query
};
let app = move |cx| {
let integration = ServerIntegration { path: path.clone() };
provide_context(cx, RouterIntegrationContext::new(integration));
view! { cx, <App/> }
};
let head = r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<script type="module">import init, { main } from '/pkg/leptos_hackernews.js'; init().then(main);</script>"#;
let tail = "</body></html>";
HttpResponse::Ok().content_type("text/html").streaming(
futures::stream::once(async { head.to_string() })
.chain(render_to_stream(move |cx| {
let app = app(cx);
let head = use_context::<MetaContext>(cx)
.map(|meta| meta.dehydrate())
.unwrap_or_default();
format!("{head}</head><body>{app}")
}))
.chain(futures::stream::once(async { tail.to_string() }))
.map(|html| Ok(web::Bytes::from(html)) as Result<web::Bytes>),
)
actix_files::NamedFile::open_async("./style.css").await
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let host = std::env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
let port = std::env::var("PORT").unwrap_or_else(|_| "8080".to_string());
log::debug!("serving at {host}:{port}");
let addr = SocketAddr::from(([127,0,0,1],3000));
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
// uncomment these lines (and .bind_openssl() below) to enable HTTPS, which is sometimes
// necessary for proper HTTP/2 streaming
// load TLS keys
// to create a self-signed temporary cert for testing:
// `openssl req -x509 -newkey rsa:4096 -nodes -keyout key.pem -out cert.pem -days 365 -subj '/CN=localhost'`
// let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
// builder
// .set_private_key_file("key.pem", SslFiletype::PEM)
// .unwrap();
// builder.set_certificate_chain_file("cert.pem").unwrap();
HttpServer::new(|| {
HttpServer::new(move || {
let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/leptos_hackernews").reload_port(3001).socket_address(addr.clone()).environment(&env::var("RUST_ENV")).build();
render_options.write_to_file();
App::new()
.service(Files::new("/pkg", "./pkg"))
.service(css)
.service(
web::scope("/pkg")
.service(Files::new("", "./pkg"))
.wrap(middleware::Compress::default()),
)
.service(render_app)
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.route("/{tail:.*}", leptos_actix::render_app_to_stream(render_options, |cx| view! { cx, <App/> }))
//.wrap(middleware::Compress::default())
})
.bind(("127.0.0.1", 8080))?
// replace .bind with .bind_openssl to use HTTPS
//.bind_openssl(&format!("{}:{}", host, port), builder)?
.bind(&addr)?
.run()
.await
}
}
// client-only stuff for Trunk
else {
use leptos_hackernews::*;
pub fn main() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| {
view! { cx, <App/> }
});
} else {
fn main() {
// no client-side main function
}
}
}

View File

@@ -6,7 +6,7 @@ pub fn Nav(cx: Scope) -> Element {
view! { cx,
<header class="header">
<nav class="inner">
<A href="/">
<A href="/" class="home".to_string()>
<strong>"HN"</strong>
</A>
<A href="/new">

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

@@ -9,6 +9,7 @@ cfg_if! {
use actix_files::{Files};
use actix_web::*;
use crate::todo::*;
use std::{ net::SocketAddr,env };
#[get("/style.css")]
async fn css() -> impl Responder {
@@ -24,16 +25,19 @@ cfg_if! {
.expect("could not run SQLx migrations");
crate::todo::register_server_functions();
let addr = SocketAddr::from(([127,0,0,1],3000));
HttpServer::new(|| {
HttpServer::new(move || {
let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/todo_app_sqlite").reload_port(3001).socket_address(addr.clone()).environment(&env::var("RUST_ENV")).build();
render_options.write_to_file();
App::new()
.service(Files::new("/pkg", "./pkg"))
.service(css)
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.route("/{tail:.*}", leptos_actix::render_app_to_stream("todo_app_cbor", |cx| view! { cx, <TodoApp/> }))
.route("/{tail:.*}", leptos_actix::render_app_to_stream(render_options, |cx| view! { cx, <TodoApp/> }))
//.wrap(middleware::Compress::default())
})
.bind(("127.0.0.1", 8081))?
.bind(&addr)?
.run()
.await
}

View File

@@ -14,10 +14,11 @@ if #[cfg(feature = "ssr")] {
use todo_app_sqlite_axum::*;
use http::StatusCode;
use tower_http::services::ServeDir;
use std::env;
#[tokio::main]
async fn main() {
let addr = SocketAddr::from(([127, 0, 0, 1], 8082));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
log::debug!("serving at {addr}");
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
@@ -45,17 +46,19 @@ if #[cfg(feature = "ssr")] {
}
let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/todo_app_sqlite_axum").socket_address(addr).reload_port(3001).environment(&env::var("RUST_ENV")).build();
render_options.write_to_file();
// build our application with a route
let app = Router::new()
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.nest_service("/pkg", pkg_service)
.nest_service("/static", static_service)
.fallback(leptos_axum::render_app_to_stream("todo_app_sqlite_axum", |cx| view! { cx, <TodoApp/> }));
.fallback(leptos_axum::render_app_to_stream(render_options.clone(), |cx| view! { cx, <TodoApp/> }));
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on {}", addr);
axum::Server::bind(&addr)
log!("listening on {}", &render_options.socket_address);
axum::Server::bind(&render_options.socket_address)
.serve(app.into_make_service())
.await
.unwrap();

View File

@@ -12,9 +12,9 @@ cfg_if! {
}
pub fn register_server_functions() {
GetTodos::register();
AddTodo::register();
DeleteTodo::register();
_ = GetTodos::register();
_ = AddTodo::register();
_ = DeleteTodo::register();
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
@@ -34,7 +34,7 @@ cfg_if! {
}
#[server(GetTodos, "/api")]
pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
pub async fn get_todos(_cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
// this is just an example of how to access server context injected in the handlers
// http::Request doesn't implement Clone, so more work will be needed to do use_context() on this
// let req = use_context::<http::Request<axum::body::BoxBody>>(cx)
@@ -70,7 +70,7 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
.execute(&mut conn)
.await
{
Ok(row) => Ok(()),
Ok(_row) => Ok(()),
Err(e) => Err(ServerFnError::ServerError(e.to_string())),
}
}
@@ -167,7 +167,7 @@ pub fn Todos(cx: Scope) -> Element {
<li>
{todo.title}
<ActionForm action=delete_todo.clone()>
<input type="hidden" name="id" value={todo.id}/>
<input type="hidden" name="id" value=todo.id/>
<input type="submit" value="X"/>
</ActionForm>
</li>

View File

@@ -1,3 +1,5 @@
use std::net::SocketAddr;
use cfg_if::cfg_if;
use leptos::*;
mod todo;
@@ -9,6 +11,7 @@ cfg_if! {
use actix_files::{Files};
use actix_web::*;
use crate::todo::*;
use std::env;
#[get("/style.css")]
async fn css() -> impl Responder {
@@ -25,15 +28,19 @@ cfg_if! {
crate::todo::register_server_functions();
HttpServer::new(|| {
let addr = SocketAddr::from(([127,0,0,1],3000));
HttpServer::new(move || {
let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/todo_app_sqlite").reload_port(3001).socket_address(addr.clone()).environment(&env::var("RUST_ENV")).build();
render_options.write_to_file();
App::new()
.service(Files::new("/pkg", "./pkg"))
.service(css)
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.route("/{tail:.*}", leptos_actix::render_app_to_stream("todo_app_sqlite", |cx| view! { cx, <TodoApp/> }))
.route("/{tail:.*}", leptos_actix::render_app_to_stream(render_options, |cx| view! { cx, <TodoApp/> }))
//.wrap(middleware::Compress::default())
})
.bind(("127.0.0.1", 8083))?
.bind(&addr)?
.run()
.await
}

View File

@@ -46,6 +46,9 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
let mut conn = db().await?;
// fake API delay
std::thread::sleep(std::time::Duration::from_millis(350));
let mut todos = Vec::new();
let mut rows = sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
while let Some(row) = rows
@@ -66,7 +69,7 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
let mut conn = db().await?;
// fake API delay
std::thread::sleep(std::time::Duration::from_millis(1250));
std::thread::sleep(std::time::Duration::from_millis(350));
sqlx::query("INSERT INTO todos (title, completed) VALUES ($1, false)")
.bind(title)
@@ -139,7 +142,7 @@ pub fn Todos(cx: Scope) -> Element {
<input type="submit" value="Add"/>
</MultiActionForm>
<div>
<Suspense fallback=view! {cx, <p>"Loading..."</p> }>
<Transition fallback=view! {cx, <p>"Loading..."</p> }>
{
let delete_todo = delete_todo.clone();
move || {
@@ -208,7 +211,7 @@ pub fn Todos(cx: Scope) -> Element {
}
}
}
</Suspense>
</Transition>
</div>
</div>
}

View File

@@ -26,7 +26,7 @@ fn Tests(cx: Scope) -> Element {
view! {
cx,
<div>
<div><SelfUpdatingEffect/></div>
//<div><SelfUpdatingEffect/></div>
<div><BlockOrders/></div>
//<div><TemplateConsumer/></div>
</div>

View File

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

View File

@@ -63,7 +63,10 @@ pub fn handle_server_fns() -> Route {
runtime.dispose();
let mut res: HttpResponseBuilder;
if accept_header.is_some() {
if accept_header == Some("application/json")
|| accept_header == Some("application/x-www-form-urlencoded")
|| accept_header == Some("application/cbor")
{
res = HttpResponse::Ok()
}
// otherwise, it's probably a <form> submit or something: redirect back to the referrer
@@ -87,7 +90,7 @@ pub fn handle_server_fns() -> Route {
res.body(data)
}
Payload::Json(data) => {
res.content_type("application/jsoon");
res.content_type("application/json");
res.body(data)
}
}
@@ -116,6 +119,7 @@ pub fn handle_server_fns() -> Route {
/// ```
/// use actix_web::{HttpServer, App};
/// use leptos::*;
/// use std::{env,net::SocketAddr};
///
/// #[component]
/// fn MyApp(cx: Scope) -> Element {
@@ -125,23 +129,28 @@ pub fn handle_server_fns() -> Route {
/// # if false { // don't actually try to run a server in a doctest...
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
/// HttpServer::new(|| {
///
/// let addr = SocketAddr::from(([127,0,0,1],3000));
/// HttpServer::new(move || {
/// let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/leptos_example").reload_port(3001).socket_address(addr.clone()).environment(&env::var("RUST_ENV")).build();
/// render_options.write_to_file();
/// App::new()
/// // {tail:.*} passes the remainder of the URL as the route
/// // the actual routing will be handled by `leptos_router`
/// .route("/{tail:.*}", leptos_actix::render_app_to_stream("leptos_example", |cx| view! { cx, <MyApp/> }))
/// .route("/{tail:.*}", leptos_actix::render_app_to_stream(render_options, |cx| view! { cx, <MyApp/> }))
/// })
/// .bind(("127.0.0.1", 8080))?
/// .bind(&addr)?
/// .run()
/// .await
/// }
/// # }
/// ```
pub fn render_app_to_stream(
client_pkg_name: &'static str,
options: RenderOptions,
app_fn: impl Fn(leptos::Scope) -> Element + Clone + 'static,
) -> Route {
web::get().to(move |req: HttpRequest| {
let options = options.clone();
let app_fn = app_fn.clone();
async move {
let path = req.path();
@@ -165,12 +174,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"
@@ -9,7 +9,9 @@ description = "Axum integrations for the Leptos web framework."
[dependencies]
axum = "0.6"
derive_builder = "0.12.0"
futures = "0.3"
kdl = "4.6.0"
leptos = { path = "../../leptos", default-features = false, version = "0.0", features = [
"ssr",
] }

View File

@@ -9,7 +9,6 @@ use leptos::*;
use leptos_meta::MetaContext;
use leptos_router::*;
use std::{io, pin::Pin, sync::Arc};
/// An Axum handlers to listens for a request with Leptos server function arguments in the body,
/// run the server function if found, and return the resulting [Response].
///
@@ -76,7 +75,11 @@ pub async fn handle_server_fns(
headers.get("Accept").and_then(|value| value.to_str().ok());
let mut res = Response::builder();
if accept_header.is_some() {
if accept_header == Some("application/json")
|| accept_header
== Some("application/x-www-form-urlencoded")
|| accept_header == Some("application/cbor")
{
res = res.status(StatusCode::OK);
}
// otherwise, it's probably a <form> submit or something: redirect back to the referrer
@@ -141,7 +144,7 @@ pub type PinnedHtmlStream = Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send>
/// ```
/// use axum::handler::Handler;
/// use axum::Router;
/// use std::net::SocketAddr;
/// use std::{net::SocketAddr, env};
/// use leptos::*;
///
/// #[component]
@@ -153,10 +156,15 @@ pub type PinnedHtmlStream = Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send>
/// #[tokio::main]
/// async fn main() {
/// let addr = SocketAddr::from(([127, 0, 0, 1], 8082));
/// let render_options: RenderOptions = RenderOptions::builder()
/// .pkg_path("/pkg/leptos_example")
/// .socket_address(addr)
/// .reload_port(3001)
/// .environment(&env::var("RUST_ENV")).build();
///
/// // build our application with a route
/// let app = Router::new()
/// .fallback(leptos_axum::render_app_to_stream("leptos_example", |cx| view! { cx, <MyApp/> }));
/// .fallback(leptos_axum::render_app_to_stream(render_options, |cx| view! { cx, <MyApp/> }));
///
/// // run our app with hyper
/// // `axum::Server` is a re-export of `hyper::Server`
@@ -167,8 +175,9 @@ pub type PinnedHtmlStream = Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send>
/// }
/// # }
/// ```
///
pub fn render_app_to_stream(
client_pkg_name: &'static str,
options: RenderOptions,
app_fn: impl Fn(leptos::Scope) -> Element + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
@@ -178,6 +187,7 @@ pub fn render_app_to_stream(
+ 'static {
move |req: Request<Body>| {
Box::pin({
let options = options.clone();
let app_fn = app_fn.clone();
async move {
// Need to get the path and query string of the Request
@@ -191,13 +201,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>";

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos"
version = "0.0.19"
version = "0.0.20"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@@ -9,11 +9,12 @@ description = "Leptos is a full-stack, isomorphic Rust web framework leveraging
readme = "../README.md"
[dependencies]
leptos_core = { path = "../leptos_core", default-features = false, version = "0.0.19" }
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.19" }
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.19" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.19" }
leptos_server = { path = "../leptos_server", default-features = false, version = "0.0.19" }
leptos_core = { path = "../leptos_core", default-features = false, version = "0.0.20" }
leptos_config = { path = "../leptos_config", default-features = false, version = "0.0.20" }
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.20" }
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.20" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.20" }
leptos_server = { path = "../leptos_server", default-features = false, version = "0.0.20" }
[build-dependencies]
rustc_version = "0.4"
@@ -21,32 +22,32 @@ rustc_version = "0.4"
[features]
default = ["csr", "serde", "interning"]
csr = [
"leptos_core/csr",
"leptos_dom/csr",
"leptos_macro/csr",
"leptos_reactive/csr",
"leptos_server/csr",
"leptos_core/csr",
"leptos_dom/csr",
"leptos_macro/csr",
"leptos_reactive/csr",
"leptos_server/csr",
]
hydrate = [
"leptos_core/hydrate",
"leptos_dom/hydrate",
"leptos_macro/hydrate",
"leptos_reactive/hydrate",
"leptos_server/hydrate",
"leptos_core/hydrate",
"leptos_dom/hydrate",
"leptos_macro/hydrate",
"leptos_reactive/hydrate",
"leptos_server/hydrate",
]
ssr = [
"leptos_core/ssr",
"leptos_dom/ssr",
"leptos_macro/ssr",
"leptos_reactive/ssr",
"leptos_server/ssr",
"leptos_core/ssr",
"leptos_dom/ssr",
"leptos_macro/ssr",
"leptos_reactive/ssr",
"leptos_server/ssr",
]
stable = [
"leptos_core/stable",
"leptos_dom/stable",
"leptos_macro/stable",
"leptos_reactive/stable",
"leptos_server/stable",
"leptos_core/stable",
"leptos_dom/stable",
"leptos_macro/stable",
"leptos_reactive/stable",
"leptos_server/stable",
]
serde = ["leptos_reactive/serde"]
serde-lite = ["leptos_reactive/serde-lite"]

View File

@@ -141,6 +141,7 @@
//! # }
//! ```
pub use leptos_config::*;
pub use leptos_core::*;
pub use leptos_dom;
pub use leptos_dom::wasm_bindgen::{JsCast, UnwrapThrowExt};

11
leptos_config/Cargo.toml Normal file
View File

@@ -0,0 +1,11 @@
[package]
name = "leptos_config"
version = "0.0.20"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/gbj/leptos"
description = "Configuraiton for the Leptos web framework."
[dependencies]
typed-builder = "0.11.0"

110
leptos_config/src/lib.rs Normal file
View File

@@ -0,0 +1,110 @@
use std::{env::VarError, net::SocketAddr, str::FromStr};
use typed_builder::TypedBuilder;
/// This struct serves as a convenient place to store details used for rendering.
/// It's serialized into a file in the root called `.leptos.kdl` for cargo-leptos
/// to watch. It's also used in our actix and axum integrations to generate the
/// correct path for WASM, JS, and Websockets. Its goal is to be the single source
/// of truth for render options
#[derive(TypedBuilder, Clone)]
pub struct RenderOptions {
/// The path and name of the WASM and JS files generated by wasm-bindgen
/// For example, `/pkg/app` might be a valid input if your crate name was `app`.
#[builder(setter(into))]
pub pkg_path: String,
/// Used to control whether the Websocket code for code watching is included.
/// I recommend passing in the result of `env::var("RUST_ENV")`
#[builder(setter(into), default)]
pub environment: RustEnv,
/// Provides a way to control the address leptos is served from.
/// Using an env variable here would allow you to run the same code in dev and prod
/// Defaults to `127.0.0.1:3000`
#[builder(setter(into), default=SocketAddr::from(([127,0,0,1], 3000)))]
pub socket_address: SocketAddr,
/// The port the Websocket watcher listens on. Should match the `reload_port` in cargo-leptos(if using).
/// Defaults to `3001`
#[builder(default = 3001)]
pub reload_port: u32,
}
impl RenderOptions {
/// Creates a hidden file at ./.leptos_toml so cargo-leptos can monitor settings. We do not read from this file
/// only write to it, you'll want to change the settings in your main function when you create RenderOptions
pub fn write_to_file(&self) {
use std::fs;
let options = format!(
r#"// This file is auto-generated. Changing it will have no effect on leptos. Change these by changing RenderOptions and rerunning
RenderOptions {{
pkg-path "{}"
environment "{:?}"
socket-address "{:?}"
reload-port {:?}
}}
"#,
self.pkg_path, self.environment, self.socket_address, self.reload_port
);
fs::write("./.leptos.kdl", options).expect("Unable to write file");
}
}
/// An enum that can be used to define the environment Leptos is running in. Can be passed to RenderOptions.
/// Setting this to the PROD variant will not include the websockets code for cargo-leptos' watch.
/// Defaults to PROD
#[derive(Debug, Clone)]
pub enum RustEnv {
PROD,
DEV,
}
impl Default for RustEnv {
fn default() -> Self {
Self::PROD
}
}
impl FromStr for RustEnv {
type Err = ();
fn from_str(input: &str) -> Result<Self, Self::Err> {
let sanitized = input.to_lowercase();
match sanitized.as_ref() {
"dev" => Ok(Self::DEV),
"development" => Ok(Self::DEV),
"prod" => Ok(Self::PROD),
"production" => Ok(Self::PROD),
_ => Ok(Self::PROD),
}
}
}
impl From<&str> for RustEnv {
fn from(str: &str) -> Self {
let sanitized = str.to_lowercase();
match sanitized.as_str() {
"dev" => Self::DEV,
"development" => Self::DEV,
"prod" => Self::PROD,
"production" => Self::PROD,
_ => {
panic!("Environment var is not recognized. Maybe try `dev` or `prod`")
}
}
}
}
impl From<&Result<String, VarError>> for RustEnv {
fn from(input: &Result<String, VarError>) -> Self {
match input {
Ok(str) => {
let sanitized = str.to_lowercase();
match sanitized.as_ref() {
"dev" => Self::DEV,
"development" => Self::DEV,
"prod" => Self::PROD,
"production" => Self::PROD,
_ => {
panic!("Environment var is not recognized. Maybe try `dev` or `prod`")
}
}
}
Err(_) => Self::PROD,
}
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_core"
version = "0.0.19"
version = "0.0.20"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@@ -8,9 +8,9 @@ repository = "https://github.com/gbj/leptos"
description = "Core functionality for the Leptos web framework."
[dependencies]
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.19" }
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.19" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.19" }
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.20" }
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.20" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.20" }
log = "0.4"
typed-builder = "0.11"

View File

@@ -1,14 +1,13 @@
use leptos_dom::Element;
use leptos_macro::*;
use leptos_reactive::{Memo, Scope};
use std::fmt::Debug;
use std::hash::Hash;
use crate as leptos;
use crate::map::map_keyed;
use typed_builder::TypedBuilder;
/// Properties for the [For](crate::For) component, a keyed list.
#[derive(Props)]
#[derive(TypedBuilder)]
pub struct ForProps<E, T, G, I, K>
where
E: Fn() -> Vec<T>,

View File

@@ -12,6 +12,7 @@ pub use for_component::*;
pub use map::*;
pub use suspense::*;
pub use transition::*;
pub use typed_builder;
/// Describes the properties of a component. This is typically generated by the `Prop` derive macro
/// as part of the `#[component]` macro.

View File

@@ -1,11 +1,10 @@
use crate as leptos;
use leptos_dom::{Child, IntoChild};
use leptos_macro::Props;
use leptos_reactive::{provide_context, Scope, SuspenseContext};
use typed_builder::TypedBuilder;
/// Props for the [Suspense](crate::Suspense) component, which shows a fallback
/// while [Resource](leptos_reactive::Resource)s are being read.
#[derive(Props)]
#[derive(TypedBuilder)]
pub struct SuspenseProps<F, E, G>
where
F: IntoChild + Clone,

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_dom"
version = "0.0.19"
version = "0.0.20"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@@ -12,7 +12,7 @@ cfg-if = "1"
futures = "0.3"
html-escape = "0.2"
js-sys = "0.3"
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.19" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.20" }
serde_json = "1"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4.31"

View File

@@ -1,5 +1,6 @@
use std::time::Duration;
use cfg_if::cfg_if;
use wasm_bindgen::convert::FromWasmAbi;
use wasm_bindgen::{prelude::Closure, JsCast, JsValue, UnwrapThrowExt};
@@ -254,30 +255,58 @@ pub fn set_interval(
Ok(IntervalHandle(handle))
}
/// Adds an event listener to the target DOM element using implicit event delegation.
pub fn add_event_listener<E>(
target: &web_sys::Element,
event_name: &'static str,
cb: impl FnMut(E) + 'static,
) where
E: FromWasmAbi + 'static,
{
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
let key = event_delegation::event_delegation_key(event_name);
_ = js_sys::Reflect::set(target, &JsValue::from_str(&key), &cb);
event_delegation::add_event_listener(event_name);
}
cfg_if! {
if #[cfg(not(feature = "stable"))] {
/// Adds an event listener to the target DOM element using implicit event delegation.
pub fn add_event_listener<E>(
target: &web_sys::Element,
event_name: &'static str,
cb: impl FnMut(E) + 'static,
) where
E: FromWasmAbi + 'static,
{
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
let key = event_delegation::event_delegation_key(event_name);
_ = js_sys::Reflect::set(target, &JsValue::from_str(&key), &cb);
event_delegation::add_event_listener(event_name);
}
#[doc(hidden)]
pub fn add_event_listener_undelegated<E>(
target: &web_sys::Element,
event_name: &'static str,
cb: impl FnMut(E) + 'static,
) where
E: FromWasmAbi + 'static,
{
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
_ = target.add_event_listener_with_callback(event_name, cb.unchecked_ref());
#[doc(hidden)]
pub fn add_event_listener_undelegated<E>(
target: &web_sys::Element,
event_name: &'static str,
cb: impl FnMut(E) + 'static,
) where
E: FromWasmAbi + 'static,
{
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
_ = target.add_event_listener_with_callback(event_name, cb.unchecked_ref());
}
} else {
/// Adds an event listener to the target DOM element using implicit event delegation.
pub fn add_event_listener(
target: &web_sys::Element,
event_name: &'static str,
cb: impl FnMut(web_sys::Event) + 'static,
)
{
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(web_sys::Event)>).into_js_value();
let key = event_delegation::event_delegation_key(event_name);
_ = js_sys::Reflect::set(target, &JsValue::from_str(&key), &cb);
event_delegation::add_event_listener(event_name);
}
#[doc(hidden)]
pub fn add_event_listener_undelegated(
target: &web_sys::Element,
event_name: &'static str,
cb: impl FnMut(web_sys::Event) + 'static,
)
{
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(web_sys::Event)>).into_js_value();
_ = target.add_event_listener_with_callback(event_name, cb.unchecked_ref());
}
}
}
#[doc(hidden)]

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_macro"
version = "0.0.19"
version = "0.0.20"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@@ -12,15 +12,16 @@ proc-macro = true
[dependencies]
cfg-if = "1"
itertools = "0.10"
proc-macro-error = "1"
proc-macro2 = "1"
quote = "1"
syn = { version = "1", features = ["full", "parsing", "extra-traits"] }
syn-rsx = "0.9"
uuid = { version = "1", features = ["v4"] }
leptos_dom = { path = "../leptos_dom", version = "0.0.19" }
leptos_reactive = { path = "../leptos_reactive", version = "0.0.19" }
leptos_server = { path = "../leptos_server", version = "0.0.19" }
leptos_dom = { path = "../leptos_dom", version = "0.0.20" }
leptos_reactive = { path = "../leptos_reactive", version = "0.0.20" }
leptos_server = { path = "../leptos_server", version = "0.0.20" }
lazy_static = "1.4"
[dev-dependencies]

View File

@@ -1,12 +1,17 @@
// Credit to Dioxus: https://github.com/DioxusLabs/dioxus/blob/master/packages/core-macro/src/inlineprops.rs
// Based in large part on Dioxus: https://github.com/DioxusLabs/dioxus/blob/master/packages/core-macro/src/inlineprops.rs
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens, TokenStreamExt};
#![allow(unstable_name_collisions)]
use std::collections::HashMap;
use proc_macro2::{Span, TokenStream as TokenStream2, TokenTree};
use quote::{quote, ToTokens, TokenStreamExt,};
use syn::{
parse::{Parse, ParseStream},
punctuated::Punctuated,
*,
};
use itertools::Itertools;
pub struct InlinePropsBody {
pub attrs: Vec<Attribute>,
@@ -21,6 +26,7 @@ pub struct InlinePropsBody {
pub output: ReturnType,
pub where_clause: Option<WhereClause>,
pub block: Box<Block>,
pub doc_comment: String
}
/// The custom rusty variant of parsing rsx!
@@ -57,6 +63,24 @@ impl Parse for InlinePropsBody {
let block = input.parse()?;
let doc_comment = attrs.iter().filter_map(|attr| if attr.path.segments[0].ident == "doc" {
Some(attr.clone().tokens.into_iter().filter_map(|token| if let TokenTree::Literal(_) = token {
// remove quotes
let chars = token.to_string();
let mut chars = chars.chars();
chars.next();
chars.next_back();
Some(chars.as_str().to_string())
} else {
None
}).collect::<String>())
} else {
None
})
.intersperse_with(|| "\n".to_string())
.collect();
Ok(Self {
vis,
fn_token,
@@ -69,6 +93,7 @@ impl Parse for InlinePropsBody {
block,
cx_token,
attrs,
doc_comment
})
}
}
@@ -86,29 +111,86 @@ impl ToTokens for InlinePropsBody {
block,
cx_token,
attrs,
doc_comment,
..
} = self;
let field_docs: HashMap<String, String> = {
let mut map = HashMap::new();
let mut pieces = doc_comment.split("# Props");
pieces.next();
let rest = pieces.next().unwrap_or_default();
let mut current_field_name = String::new();
let mut current_field_value = String::new();
for line in rest.split('\n') {
if let Some(line) = line.strip_prefix(" - ") {
let mut pieces = line.split("**");
pieces.next();
let field_name = pieces.next();
let field_value = pieces.next().unwrap_or_default();
let field_value = if let Some((_ty, desc)) = field_value.split_once('-') {
desc
} else {
field_value
};
if let Some(field_name) = field_name {
if !current_field_name.is_empty() {
map.insert(current_field_name.clone(), current_field_value.clone());
}
current_field_name = field_name.to_string();
current_field_value = String::new();
current_field_value.push_str(field_value);
} else {
current_field_value.push_str(field_value);
}
} else {
current_field_value.push_str(line);
}
}
if !current_field_name.is_empty() {
map.insert(current_field_name, current_field_value.clone());
}
map
};
let fields = inputs.iter().map(|f| {
let typed_arg = match f {
FnArg::Receiver(_) => todo!(),
FnArg::Typed(t) => t,
};
let comment = if let Pat::Ident(ident) = &*typed_arg.pat {
field_docs.get(&ident.ident.to_string()).cloned()
} else {
None
}.unwrap_or_default();
let comment_macro = quote! {
#[doc = #comment]
};
if let Type::Path(pat) = &*typed_arg.ty {
if pat.path.segments[0].ident == "Option" {
quote! {
#[builder(default, setter(strip_option))]
#vis #f
#comment_macro
#[builder(default, setter(strip_option, doc = #comment))]
pub #f
}
} else {
quote! { #vis #f }
quote! {
#comment_macro
#[builder(setter(doc = #comment))]
pub #f
}
}
} else {
quote! { #vis #f }
quote! {
#comment
#vis #f
}
}
});
let struct_name = Ident::new(&format!("{}Props", ident), Span::call_site());
let prop_struct_comments = format!("Props for the [`{ident}`] component.");
let field_names = inputs.iter().filter_map(|f| match f {
FnArg::Receiver(_) => todo!(),
@@ -122,7 +204,10 @@ impl ToTokens for InlinePropsBody {
};
//let modifiers = if first_lifetime.is_some() {
let modifiers = quote! { #[derive(Props)] };
let modifiers = quote! {
#[derive(leptos::typed_builder::TypedBuilder)]
#[builder(doc)]
};
/* } else {
quote! { #[derive(Props, PartialEq, Eq)] }
}; */
@@ -148,18 +233,14 @@ impl ToTokens for InlinePropsBody {
quote! { <#struct_generics> },
)
} else {
let lifetime: LifetimeDef = parse_quote! { 'a };
let fn_generics = generics.clone();
let mut fn_generics = generics.clone();
fn_generics
.params
.insert(0, GenericParam::Lifetime(lifetime.clone()));
(quote! { #lifetime, }, fn_generics, quote! { #generics })
(quote! { }, fn_generics, quote! { #generics })
};
out_tokens.append_all(quote! {
#modifiers
#[doc = #prop_struct_comments]
#[allow(non_camel_case_types)]
#vis struct #struct_name #struct_generics
#where_clause

View File

@@ -3,7 +3,6 @@
use proc_macro::{TokenStream, TokenTree};
use quote::ToTokens;
use server::server_macro_impl;
use syn::{parse_macro_input, DeriveInput};
use syn_rsx::{parse, NodeElement};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -34,7 +33,6 @@ mod params;
mod view;
use view::render_view;
mod component;
mod props;
mod server;
/// The `view` macro uses RSX (like JSX, but Rust!) It follows most of the
@@ -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

@@ -564,14 +564,28 @@ fn attr_to_tokens(
let event_type = event_type.parse::<TokenStream>().expect("couldn't parse event name");
if mode != Mode::Ssr {
if NON_BUBBLING_EVENTS.contains(&name.as_str()) {
expressions.push(quote_spanned! {
span => ::leptos::add_event_listener_undelegated::<web_sys::#event_type>(#el_id.unchecked_ref(), #name, #handler);
});
} else {
expressions.push(quote_spanned! {
span => ::leptos::add_event_listener::<web_sys::#event_type>(#el_id.unchecked_ref(), #name, #handler);
});
cfg_if::cfg_if! {
if #[cfg(feature = "stable")] {
if NON_BUBBLING_EVENTS.contains(&name.as_str()) {
expressions.push(quote_spanned! {
span => ::leptos::add_event_listener_undelegated(#el_id.unchecked_ref(), #name, #handler);
});
} else {
expressions.push(quote_spanned! {
span => ::leptos::add_event_listener(#el_id.unchecked_ref(), #name, #handler);
});
}
} else {
if NON_BUBBLING_EVENTS.contains(&name.as_str()) {
expressions.push(quote_spanned! {
span => ::leptos::add_event_listener_undelegated::<web_sys::#event_type>(#el_id.unchecked_ref(), #name, #handler);
});
} else {
expressions.push(quote_spanned! {
span => ::leptos::add_event_listener::<web_sys::#event_type>(#el_id.unchecked_ref(), #name, #handler);
});
}
}
}
} else {
@@ -865,9 +879,12 @@ fn block_to_tokens(
//next_sib = Some(el.clone());
template.push_str("<!#><!/>");
let end = Ident::new(&format!("{co}_end"), span);
navigations.push(quote! {
#location;
let (#el, #co) = #cx.get_next_marker(&#name);
let #end = #co.last().cloned().unwrap_or_else(|| #el.next_sibling().unwrap_throw());
//log::debug!("get_next_marker => {}", #el.node_name());
});
@@ -881,6 +898,8 @@ fn block_to_tokens(
);
});
return PrevSibChange::Sib(end);
//current = Some(el);
}
// in SSR, it needs to insert the value, wrapped in comments

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_server"
version = "0.0.19"
version = "0.0.20"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@@ -8,8 +8,8 @@ repository = "https://github.com/gbj/leptos"
description = "RPC for the Leptos web framework."
[dependencies]
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.19" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.19" }
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.20" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.20" }
form_urlencoded = "1"
gloo-net = "0.2"
lazy_static = "1"

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.0.4"
version = "0.0.5"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@@ -21,3 +21,7 @@ default = ["csr"]
csr = ["leptos/csr"]
hydrate = ["leptos/hydrate"]
ssr = ["leptos/ssr"]
stable = ["leptos/stable"]
[package.metadata.cargo-all-features]
denylist = ["stable"]

View File

@@ -70,16 +70,16 @@ impl MetaTagsContext {
pub struct MetaProps {
/// The [`charset`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-charset) attribute.
#[builder(default, setter(strip_option, into))]
charset: Option<TextProp>,
pub charset: Option<TextProp>,
/// The [`name`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-name) attribute.
#[builder(default, setter(strip_option, into))]
name: Option<TextProp>,
pub name: Option<TextProp>,
/// The [`http-equiv`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-http-equiv) attribute.
#[builder(default, setter(strip_option, into))]
http_equiv: Option<TextProp>,
pub http_equiv: Option<TextProp>,
/// The [`content`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-content) attribute.
#[builder(default, setter(strip_option, into))]
content: Option<TextProp>,
pub content: Option<TextProp>,
}
/// Injects an [HTMLMetaElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMetaElement) into the document

View File

@@ -26,7 +26,7 @@ impl StylesheetContext {
pub struct StylesheetProps {
/// The URL at which the stylesheet can be located.
#[builder(setter(into))]
href: String,
pub href: String,
}
/// Injects an [HTMLLinkElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document

View File

@@ -50,10 +50,10 @@ where
pub struct TitleProps {
/// A function that will be applied to any text value before its set as the title.
#[builder(default, setter(strip_option, into))]
formatter: Option<Formatter>,
// Sets the the current `document.title`.
pub formatter: Option<Formatter>,
/// Sets the the current `document.title`.
#[builder(default, setter(strip_option, into))]
text: Option<TextProp>,
pub text: Option<TextProp>,
}
/// A component to set the documents title by creating an [HTMLTitleElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTitleElement).

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.0.5"
version = "0.0.6"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@@ -21,7 +21,6 @@ bincode = "1"
url = { version = "2", optional = true }
urlencoding = "2"
thiserror = "1"
typed-builder = "0.10"
serde_urlencoded = "0.7"
serde = "1"
js-sys = { version = "0.3" }
@@ -58,7 +57,8 @@ default = ["csr"]
csr = ["leptos/csr"]
hydrate = ["leptos/hydrate"]
ssr = ["leptos/ssr", "dep:url", "dep:regex"]
stable = ["leptos/stable"]
[package.metadata.cargo-all-features]
# No need to test optional dependencies as they are enabled by the ssr feature
denylist = ["url", "regex"]
denylist = ["url", "regex", "stable"]

View File

@@ -1,7 +1,8 @@
use crate::{use_navigate, use_resolved_path, ToHref};
use crate::{use_navigate, use_resolved_path, TextProp};
use cfg_if::cfg_if;
use leptos::*;
use leptos::typed_builder::*;
use std::{error::Error, rc::Rc};
use typed_builder::TypedBuilder;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
@@ -11,7 +12,7 @@ use wasm_bindgen_futures::JsFuture;
#[derive(TypedBuilder)]
pub struct FormProps<A>
where
A: ToHref + 'static,
A: TextProp + 'static,
{
/// [`method`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-method)
/// is the HTTP method to submit the form with (`get` or `post`).
@@ -48,7 +49,7 @@ where
#[allow(non_snake_case)]
pub fn Form<A>(cx: Scope, props: FormProps<A>) -> Element
where
A: ToHref + 'static,
A: TextProp + 'static,
{
let FormProps {
method,
@@ -62,7 +63,7 @@ where
} = props;
let action_version = version;
let action = use_resolved_path(cx, move || action.to_href()());
let action = use_resolved_path(cx, move || action.to_value()());
let on_submit = move |ev: web_sys::SubmitEvent| {
if ev.default_prevented() {
@@ -131,15 +132,37 @@ where
let children = children();
view! { cx,
<form
method=method
action=action
enctype=enctype
on:submit=on_submit
>
{children}
</form>
cfg_if! {
if #[cfg(feature = "stable")] {
let on_submit = move |ev: web_sys::Event| on_submit(ev.unchecked_into());
}
};
cfg_if! {
if #[cfg(not(feature = "stable"))] {
view! { cx,
<form
method=method
action=action
enctype=enctype
on:submit=on_submit
>
{children}
</form>
}
}
else {
view! { cx,
<form
method=method
action=move || action.get()
enctype=enctype
on:submit=on_submit
>
{children}
</form>
}
}
}
}
@@ -282,6 +305,12 @@ where
let children = (props.children)();
cfg_if! {
if #[cfg(feature = "stable")] {
let on_submit = move |ev: web_sys::Event| on_submit(ev.unchecked_into());
}
};
view! { cx,
<form
method="POST"

View File

@@ -1,7 +1,7 @@
use cfg_if::cfg_if;
use leptos::leptos_dom::IntoChild;
use leptos::*;
use typed_builder::TypedBuilder;
use leptos::typed_builder::*;
#[cfg(any(feature = "csr", feature = "hydrate"))]
use wasm_bindgen::JsCast;
@@ -10,31 +10,31 @@ use crate::{use_location, use_resolved_path, State};
/// Describes a value that is either a static or a reactive URL, i.e.,
/// a [String], a [&str], or a reactive `Fn() -> String`.
pub trait ToHref {
pub trait TextProp {
/// Converts the (static or reactive) URL into a function that can be called to
/// return the URL.
fn to_href(&self) -> Box<dyn Fn() -> String + '_>;
fn to_value(&self) -> Box<dyn Fn() -> String + '_>;
}
impl ToHref for &str {
fn to_href(&self) -> Box<dyn Fn() -> String> {
impl TextProp for &str {
fn to_value(&self) -> Box<dyn Fn() -> String> {
let s = self.to_string();
Box::new(move || s.clone())
}
}
impl ToHref for String {
fn to_href(&self) -> Box<dyn Fn() -> String> {
impl TextProp for String {
fn to_value(&self) -> Box<dyn Fn() -> String> {
let s = self.clone();
Box::new(move || s.clone())
}
}
impl<F> ToHref for F
impl<F> TextProp for F
where
F: Fn() -> String + 'static,
{
fn to_href(&self) -> Box<dyn Fn() -> String + '_> {
fn to_value(&self) -> Box<dyn Fn() -> String + '_> {
Box::new(self)
}
}
@@ -46,7 +46,7 @@ where
pub struct AProps<C, H>
where
C: IntoChild,
H: ToHref + 'static,
H: TextProp + 'static,
{
/// Used to calculate the link's `href` attribute. Will be resolved relative
/// to the current route.
@@ -56,26 +56,37 @@ where
#[builder(default)]
pub exact: bool,
/// An object of any type that will be pushed to router state
#[builder(default, setter(strip_option))]
#[builder(default, setter(strip_option, into))]
pub state: Option<State>,
/// If `true`, the link will not add to the browser's history (so, pressing `Back`
/// will skip this page.)
#[builder(default)]
pub replace: bool,
/// Sets the `class` attribute on the underlying `<a>` tag, making it easier to style.
#[builder(default, setter(strip_option, into))]
pub class: Option<MaybeSignal<String>>,
/// The nodes or elements to be shown inside the link.
pub children: Box<dyn Fn() -> Vec<C>>,
}
/// An HTML [`a`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a)
/// progressively enhanced to use client-side routing.
///
/// Client-side routing also works with ordinary HTML `<a>` tags, but `<A>` does two additional things:
/// 1) Correctly resolves relative nested routes. Relative routing with ordinary `<a>` tags can be tricky.
/// For example, if you have a route like `/post/:id`, `<A href="1">` will generate the correct relative
/// route, but `<a href="1">` likely will not (depending on where it appears in your view.)
/// 2) Sets the `aria-current` attribute if this link is the active link (i.e., its a link to the page youre on).
/// This is helpful for accessibility and for styling. For example, maybe you want to set the link a
/// different color if its a link to the page youre currently on.
#[allow(non_snake_case)]
pub fn A<C, H>(cx: Scope, props: AProps<C, H>) -> Element
where
C: IntoChild,
H: ToHref + 'static,
H: TextProp + 'static,
{
let location = use_location(cx);
let href = use_resolved_path(cx, move || props.href.to_href()());
let href = use_resolved_path(cx, move || props.href.to_value()());
let is_active = create_memo(cx, move |_| match href.get() {
None => false,
@@ -99,6 +110,7 @@ where
debug_warn!("[Link] Pass exactly one child to <A/>. If you want to pass more than one child, nest them within an element.");
}
let child = children.remove(0);
let class = props.class;
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
@@ -108,6 +120,7 @@ where
prop:state={props.state.map(|s| s.to_js_value())}
prop:replace={props.replace}
aria-current=move || if is_active.get() { Some("page") } else { None }
class=move || class.as_ref().map(|class| class.get())
>
{child}
</a>
@@ -117,6 +130,7 @@ where
<a
href=move || href().unwrap_or_default()
aria-current=move || if is_active() { Some("page") } else { None }
class=move || class.as_ref().map(|class| class.get())
>
{child}
</a>

View File

@@ -1,7 +1,7 @@
use std::{borrow::Cow, rc::Rc};
use leptos::*;
use typed_builder::TypedBuilder;
use leptos::typed_builder::*;
use crate::{
matching::{resolve_path, PathMatch, RouteDefinition, RouteMatch},

View File

@@ -2,8 +2,8 @@ use cfg_if::cfg_if;
use std::{cell::RefCell, rc::Rc};
use leptos::*;
use leptos::typed_builder::*;
use thiserror::Error;
use typed_builder::TypedBuilder;
#[cfg(not(feature = "ssr"))]
use wasm_bindgen::JsCast;
@@ -68,7 +68,6 @@ impl std::fmt::Debug for RouterContextInner {
f.debug_struct("RouterContextInner")
.field("location", &self.location)
.field("base", &self.base)
.field("history", &std::any::type_name_of_val(&self.history))
.field("cx", &self.cx)
.field("reference", &self.reference)
.field("set_reference", &self.set_reference)
@@ -90,7 +89,10 @@ impl RouterContext {
let history = use_context::<RouterIntegrationContext>(cx)
.unwrap_or_else(|| RouterIntegrationContext(Rc::new(crate::BrowserIntegration {})));
} else {
let history = use_context::<RouterIntegrationContext>(cx).expect("You must call provide_context::<RouterIntegrationContext>(cx, ...) somewhere above the <Router/>.");
let history = use_context::<RouterIntegrationContext>(cx).expect("You must call provide_context::<RouterIntegrationContext>(cx, ...) somewhere above the <Router/>.\n\n \
If you are using `leptos_actix` or `leptos_axum` and seeing this message, it is a bug: \n \
1. Please check to make sure you're on the latest versions of `leptos_actix` or `leptos_axum` and of `leptos_router`. \n
2. If you're on the latest versions, please open an issue at https://github.com/gbj/leptos/issues");
}
};
@@ -103,14 +105,16 @@ impl RouterContext {
let base = base.unwrap_or_default();
let base_path = resolve_path("", base, None);
if let Some(base_path) = &base_path && source.with(|s| s.value.is_empty()) {
history.navigate(&LocationChange {
value: base_path.to_string(),
replace: true,
scroll: false,
state: State(None)
});
}
if let Some(base_path) = &base_path {
if source.with(|s| s.value.is_empty()) {
history.navigate(&LocationChange {
value: base_path.to_string(),
replace: true,
scroll: false,
state: State(None),
});
}
}
// the current URL
let (reference, set_reference) = create_signal(cx, source.with(|s| s.value.clone()));
@@ -136,9 +140,9 @@ impl RouterContext {
// 3) update the state
// this will trigger the new route match below
create_render_effect(cx, move |_| {
let LocationChange { value, state, .. } = source();
let LocationChange { value, state, .. } = source.get();
cx.untrack(move || {
if value != reference() {
if value != reference.get() {
set_reference.update(move |r| *r = value);
set_state.update(move |s| *s = state);
}

View File

@@ -1,9 +1,20 @@
use std::{cmp::Reverse, rc::Rc, cell::{RefCell, Cell}, ops::IndexMut};
use std::{
cell::{Cell, RefCell},
cmp::Reverse,
ops::IndexMut,
rc::Rc,
};
use leptos::*;
use typed_builder::TypedBuilder;
use leptos::typed_builder::*;
use crate::{matching::{expand_optionals, join_paths, Branch, Matcher, RouteDefinition, get_route_matches, RouteMatch}, RouterContext, RouteContext};
use crate::{
matching::{
expand_optionals, get_route_matches, join_paths, Branch, Matcher, RouteDefinition,
RouteMatch,
},
RouteContext, RouterContext,
};
/// Props for the [Routes] component, which contains route definitions and manages routing.
#[derive(TypedBuilder)]
@@ -13,8 +24,8 @@ pub struct RoutesProps {
children: Box<dyn Fn() -> Vec<RouteDefinition>>,
}
/// Contains route definitions and manages the actual routing process.
///
/// Contains route definitions and manages the actual routing process.
///
/// You should locate the `<Routes/>` component wherever on the page you want the routes to appear.
#[allow(non_snake_case)]
pub fn Routes(cx: Scope, props: RoutesProps) -> impl IntoChild {
@@ -34,9 +45,7 @@ pub fn Routes(cx: Scope, props: RoutesProps) -> impl IntoChild {
// whenever path changes, update matches
let matches = create_memo(cx, {
let router = router.clone();
move |_| {
get_route_matches(branches.clone(), router.pathname().get())
}
move |_| get_route_matches(branches.clone(), router.pathname().get())
});
// Rebuild the list of nested routes conservatively, and show the root route here
@@ -68,61 +77,66 @@ pub fn Routes(cx: Scope, props: RoutesProps) -> impl IntoChild {
let prev_match = prev_matches.and_then(|p| p.get(i));
let next_match = next_matches.get(i).unwrap();
if let Some(prev) = prev_routes && let Some(prev_match) = prev_match && next_match.route.key == prev_match.route.key {
let prev_one = { prev.borrow()[i].clone() };
if i >= next.borrow().len() {
next.borrow_mut().push(prev_one);
} else {
*(next.borrow_mut().index_mut(i)) = prev_one;
}
} else {
equal = false;
if i == 0 {
root_equal.set(false);
match (prev_routes, prev_match) {
(Some(prev), Some(prev_match))
if next_match.route.key == prev_match.route.key =>
{
let prev_one = { prev.borrow()[i].clone() };
if i >= next.borrow().len() {
next.borrow_mut().push(prev_one);
} else {
*(next.borrow_mut().index_mut(i)) = prev_one;
}
}
_ => {
equal = false;
if i == 0 {
root_equal.set(false);
}
let disposer = cx.child_scope({
let next = next.clone();
let router = Rc::clone(&router.inner);
move |cx| {
let disposer = cx.child_scope({
let next = next.clone();
let next_ctx = RouteContext::new(
cx,
&RouterContext { inner: router },
{
let next = next.clone();
move || {
if let Some(route_states) = use_context::<Memo<RouterState>>(cx) {
route_states.with(|route_states| {
let routes = route_states.routes.borrow();
routes.get(i + 1).cloned()
})
} else {
next.borrow().get(i + 1).cloned()
let router = Rc::clone(&router.inner);
move |cx| {
let next = next.clone();
let next_ctx = RouteContext::new(
cx,
&RouterContext { inner: router },
{
let next = next.clone();
move || {
if let Some(route_states) =
use_context::<Memo<RouterState>>(cx)
{
route_states.with(|route_states| {
let routes = route_states.routes.borrow();
routes.get(i + 1).cloned()
})
} else {
next.borrow().get(i + 1).cloned()
}
}
}
},
move || {
matches.with(|m| m.get(i).cloned())
}
);
},
move || matches.with(|m| m.get(i).cloned()),
);
if let Some(next_ctx) = next_ctx {
if next.borrow().len() > i + 1 {
next.borrow_mut()[i] = next_ctx;
} else {
next.borrow_mut().push(next_ctx);
if let Some(next_ctx) = next_ctx {
if next.borrow().len() > i + 1 {
next.borrow_mut()[i] = next_ctx;
} else {
next.borrow_mut().push(next_ctx);
}
}
}
}
});
});
if disposers.borrow().len() > i + 1 {
let mut disposers = disposers.borrow_mut();
let old_route_disposer = std::mem::replace(&mut disposers[i], disposer);
old_route_disposer.dispose();
} else {
disposers.borrow_mut().push(disposer);
if disposers.borrow().len() > i + 1 {
let mut disposers = disposers.borrow_mut();
let old_route_disposer = std::mem::replace(&mut disposers[i], disposer);
old_route_disposer.dispose();
} else {
disposers.borrow_mut().push(disposer);
}
}
}
}
@@ -134,25 +148,34 @@ pub fn Routes(cx: Scope, props: RoutesProps) -> impl IntoChild {
}
}
if let Some(prev) = &prev && equal {
RouterState {
matches: next_matches.to_vec(),
routes: prev_routes.cloned().unwrap_or_default(),
root: prev.root.clone(),
if let Some(prev) = &prev {
if equal {
RouterState {
matches: next_matches.to_vec(),
routes: prev_routes.cloned().unwrap_or_default(),
root: prev.root.clone(),
}
} else {
let root = next.borrow().get(0).cloned();
RouterState {
matches: next_matches.to_vec(),
routes: Rc::new(RefCell::new(next.borrow().to_vec())),
root,
}
}
} else {
let root = next.borrow().get(0).cloned();
RouterState {
matches: next_matches.to_vec(),
routes: Rc::new(RefCell::new(next.borrow().to_vec())),
root
root,
}
}
}
});
// show the root route
create_memo(cx, move |prev| {
let root = create_memo(cx, move |prev| {
provide_context(cx, route_states);
route_states.with(|state| {
let root = state.routes.borrow();
@@ -162,14 +185,20 @@ pub fn Routes(cx: Scope, props: RoutesProps) -> impl IntoChild {
}
if prev.is_none() || !root_equal.get() {
root.as_ref().map(|route| {
route.outlet().into_child(cx)
})
root.as_ref().map(|route| route.outlet().into_child(cx))
} else {
prev.cloned().unwrap()
}
})
})
});
cfg_if::cfg_if! {
if #[cfg(feature = "stable")] {
move || root.get()
} else {
root
}
}
}
#[derive(Clone, Debug, PartialEq)]

View File

@@ -107,36 +107,51 @@ where
fn into_param(value: Option<&str>, name: &str) -> Result<Self, ParamsError>;
}
impl<T> IntoParam for Option<T>
where
T: FromStr,
<T as FromStr>::Err: std::error::Error + 'static,
{
fn into_param(value: Option<&str>, _name: &str) -> Result<Self, ParamsError> {
match value {
None => Ok(None),
Some(value) => match T::from_str(value) {
Ok(value) => Ok(Some(value)),
Err(e) => {
eprintln!("{}", e);
Err(ParamsError::Params(Rc::new(e)))
}
},
cfg_if::cfg_if! {
if #[cfg(not(feature = "stable"))] {
auto trait NotOption {}
impl<T> !NotOption for Option<T> {}
impl<T> IntoParam for T
where
T: FromStr + NotOption,
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
{
fn into_param(value: Option<&str>, name: &str) -> Result<Self, ParamsError> {
let value = value.ok_or_else(|| ParamsError::MissingParam(name.to_string()))?;
Self::from_str(value).map_err(|e| ParamsError::Params(Rc::new(e)))
}
}
}
}
auto trait NotOption {}
impl<T> !NotOption for Option<T> {}
impl<T> IntoParam for T
where
T: FromStr + NotOption,
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
{
fn into_param(value: Option<&str>, name: &str) -> Result<Self, ParamsError> {
let value = value.ok_or_else(|| ParamsError::MissingParam(name.to_string()))?;
Self::from_str(value).map_err(|e| ParamsError::Params(Rc::new(e)))
impl<T> IntoParam for Option<T>
where
T: FromStr,
<T as FromStr>::Err: std::error::Error + 'static,
{
fn into_param(value: Option<&str>, _name: &str) -> Result<Self, ParamsError> {
match value {
None => Ok(None),
Some(value) => match T::from_str(value) {
Ok(value) => Ok(Some(value)),
Err(e) => {
eprintln!("{}", e);
Err(ParamsError::Params(Rc::new(e)))
}
},
}
}
}
} else {
impl<T> IntoParam for T
where
T: FromStr,
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
{
fn into_param(value: Option<&str>, name: &str) -> Result<Self, ParamsError> {
let value = value.ok_or_else(|| ParamsError::MissingParam(name.to_string()))?;
Self::from_str(value).map_err(|e| ParamsError::Params(Rc::new(e)))
}
}
}
}

View File

@@ -32,6 +32,9 @@
//! them with server-side rendering (with or without hydration), they just work,
//! whether JS/WASM have loaded or not.
//!
//! Note as well that client-side routing works with ordinary `<a>` tags, as well,
//! so you do not even need to use the `<A/>` component in most cases.
//!
//! ## Example
//!
//! ```rust
@@ -135,10 +138,9 @@
//!
//! ```
#![feature(auto_traits)]
#![feature(let_chains)]
#![feature(negative_impls)]
#![feature(type_name_of_val)]
#![cfg_attr(not(feature = "stable"), feature(auto_traits))]
#![cfg_attr(not(feature = "stable"), feature(negative_impls))]
#![cfg_attr(not(feature = "stable"), feature(type_name_of_val))]
mod components;
mod history;

View File

@@ -80,13 +80,15 @@ impl Matcher {
path.push_str(loc_segment);
}
if let Some(splat) = &self.splat && !splat.is_empty() {
let value = if len_diff > 0 {
loc_segments[self.len..].join("/")
} else {
"".into()
};
params.insert(splat.into(), value);
if let Some(splat) = &self.splat {
if !splat.is_empty() {
let value = if len_diff > 0 {
loc_segments[self.len..].join("/")
} else {
"".into()
};
params.insert(splat.into(), value);
}
}
Some(PathMatch { path, params })