Compare commits

..

38 Commits

Author SHA1 Message Date
Greg Johnston
10e01bf989 Remove logs I reintroduced 2022-11-06 20:43:05 -05:00
Greg Johnston
49820ccba6 This should fix the out-of-order component/element rendering in #53. 2022-11-06 20:37:09 -05:00
Greg Johnston
b9ca0b11a2 Fix breaking CI on leptos_server 2022-11-06 07:08:57 -05:00
Greg Johnston
296e27cd4a Add notes on types that can be accepted as attributes. 2022-11-06 06:54:20 -05:00
Greg Johnston
fd3443b129 Fix TodoMVC example 2022-11-05 23:27:36 -04:00
Greg Johnston
aa3dd356c1 Fix issues with action integration with forms 2022-11-05 22:47:33 -04:00
Greg Johnston
35ca30fbab 0.0.14 2022-11-05 22:39:06 -04:00
Greg Johnston
132f0839c6 Fix 0.0.13 leptos_core 2022-11-05 22:37:29 -04:00
Greg Johnston
e9c1799a11 0.0.13 2022-11-05 22:24:59 -04:00
Greg Johnston
4577313cca Include create_action in root docs 2022-11-05 22:24:54 -04:00
Greg Johnston
f75d49fe4c Router 0.0.2 2022-11-05 22:23:12 -04:00
Greg Johnston
6a38375c66 Complete docs 2022-11-05 22:18:01 -04:00
Greg Johnston
f9f4fb0fef Remove partial create_action docs from crate level 2022-11-05 22:14:28 -04:00
Greg Johnston
42cd3f1d69 Make sure server-only stuff appears in docs 2022-11-05 22:14:19 -04:00
Greg Johnston
ade2eda26d Add docs for leptos_server 2022-11-05 20:08:03 -04:00
Greg Johnston
680b6ecc20 Remove todomvc-ssr from workspace 2022-11-05 19:55:58 -04:00
Greg Johnston
4e9d0354c6 Use localStorage for initial state of todo list 2022-11-05 19:48:12 -04:00
Greg Johnston
cccd7068f9 Remove : I have bigger plans for this 2022-11-05 19:47:17 -04:00
Greg Johnston
8f56a52615 Simplify and add comments on TodoMVC 2022-11-05 19:14:45 -04:00
Greg Johnston
6c04e91088 Fix broken class: and prop: 2022-11-05 19:11:02 -04:00
Greg Johnston
9ef350c2d6 Merge pull request #50 from gbj/remove-loaders 2022-11-05 10:25:19 -04:00
Greg Johnston
f559d47714 Merge pull request #49 from gbj/router-docs 2022-11-05 10:24:56 -04:00
Greg Johnston
2483616d0d Remove route data loaders for now. 2022-11-05 09:32:12 -04:00
Greg Johnston
2595ffe10e Fix doctests 2022-11-05 09:22:02 -04:00
Greg Johnston
221cdf2685 Doc updates, cleanups 2022-11-05 09:12:42 -04:00
Greg Johnston
1cb278f520 Merge pull request #48 from mrjoe7/actions
Add `test` action configuration #19
2022-11-04 21:10:42 -04:00
Tomas Sedlak
5cfd44474d Add test action configuration 2022-11-05 00:14:30 +01:00
Greg Johnston
bd652ec542 Adding docs 2022-11-04 16:50:03 -04:00
Greg Johnston
d8852f909e Remove action 2022-11-03 21:31:32 -04:00
Greg Johnston
e16cc4fc4a Remove actions 2022-11-03 21:11:59 -04:00
Greg Johnston
d5e3661bcf Remove actions (moved to leptos_server) 2022-11-03 21:07:05 -04:00
Greg Johnston
8873ddc40a Require docs 2022-11-03 21:06:46 -04:00
Greg Johnston
b7e2e983f0 Update main docs 2022-11-03 21:06:23 -04:00
Greg Johnston
3701f65693 Add missing leptos_server metadata 2022-11-03 20:05:44 -04:00
Greg Johnston
a5712d3e17 0.0.12 2022-11-03 20:00:26 -04:00
Greg Johnston
4fba035f19 Merge pull request #47 from gbj/allow-on-dash-syntax-in-macro
Allow on-, class-, prop-, and attr- as equivalent to on:, class:, pro…
2022-11-03 19:57:56 -04:00
Greg Johnston
47fad9a042 Allow on-, class-, prop-, and attr- as equivalent to on:, class:, prop:, and attr: to get around a syn-rsx parsing limitation on mixing colons and dashes in an attribute name 2022-11-03 19:57:27 -04:00
Greg Johnston
c8545f47cb Enable cargo make build and cargo make test by removing mutually exclusive features
Remove mutually exclusive features
2022-11-03 18:32:09 -04:00
45 changed files with 880 additions and 801 deletions

51
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,51 @@
name: Test
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
jobs:
test:
name: Test on ${{ matrix.os }} (using rustc ${{ matrix.rust }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
rust:
- nightly
os:
- ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
- name: Setup cargo-make
uses: davidB/rust-cargo-make@v1
- name: Cargo generate-lockfile
run: cargo generate-lockfile
- name: Cargo cache
uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ matrix.rust }}-${{ hashFiles('**/Cargo.lock') }}
- name: Run tests with all features
run: cargo make ci

View File

@@ -26,8 +26,6 @@ members = [
"examples/parent-child",
"examples/router",
"examples/todomvc",
"examples/todomvc-ssr/todomvc-ssr-client",
"examples/todomvc-ssr/todomvc-ssr-server",
]
exclude = [
"benchmarks",

View File

@@ -5,6 +5,7 @@ pub fn simple_counter(cx: Scope) -> web_sys::Element {
view! { cx,
<div>
<MyComponent><p>"Here's the child"</p></MyComponent>
<button on:click=move |_| set_value(0)>"Clear"</button>
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
<span>"Value: " {move || value().to_string()} "!"</span>
@@ -12,3 +13,14 @@ pub fn simple_counter(cx: Scope) -> web_sys::Element {
</div>
}
}
#[component]
fn MyComponent(cx: Scope, children: Option<Box<dyn Fn() -> Vec<Element>>>) -> Element {
view! {
cx,
<my-component>
<p>"Here's the child you passed in: "</p>
<slot></slot>
</my-component>
}
}

View File

@@ -31,46 +31,26 @@ async fn render_app(req: HttpRequest) -> impl Responder {
view! { cx, <App/> }
};
let accepts_type = req.headers().get("Accept").map(|h| h.to_str());
match accepts_type {
// if asks for JSON, send the loader function JSON or 404
Some(Ok("application/json")) => {
let json = loader_to_json(app).await;
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/hackernews_client.js'; init().then(main);</script>"#;
let tail = "</body></html>";
let res = if let Some(json) = json {
HttpResponse::Ok()
.content_type("application/json")
.body(json)
} else {
HttpResponse::NotFound().body(())
};
res
}
// otherwise, send HTML
_ => {
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/hackernews_client.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>),
)
}
}
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_web::main]

View File

@@ -1,16 +0,0 @@
[package]
name = "todomvc-ssr-client"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
console_log = "0.2"
leptos = { path = "../../../leptos", default-features = false, features = ["hydrate"] }
todomvc = { path = "../../todomvc", default-features = false, features = ["hydrate"] }
log = "0.4"
wasm-bindgen = "0.2"
console_error_panic_hook = "0.1.7"

View File

@@ -1 +0,0 @@
wasm-pack build --target=web --release

View File

@@ -1,22 +0,0 @@
use leptos::*;
use todomvc::*;
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn main() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
log::debug!("initialized logging");
leptos::hydrate(body().unwrap(), |cx| {
// initial state — identical to server
let todos = Todos(vec![
Todo::new(cx, 0, "Buy milk".to_string()),
Todo::new(cx, 1, "???".to_string()),
Todo::new(cx, 2, "Profit!".to_string()),
]);
view! { cx, <TodoMVC todos=todos/> }
});
}

View File

@@ -1,10 +0,0 @@
[package]
name = "todomvc-ssr-server"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-files = "0.6"
actix-web = "4"
leptos = { path = "../../../leptos", default-features = false, features = ["ssr"] }
todomvc = { path = "../../todomvc", default-features = false, features = ["ssr"] }

View File

@@ -1,51 +0,0 @@
use actix_files::Files;
use actix_web::*;
use leptos::*;
use todomvc::*;
#[get("/")]
async fn render_todomvc() -> impl Responder {
HttpResponse::Ok().content_type("text/html").body(format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="stylesheet" href="/static/todomvc-common/base.css"/>
<link rel="stylesheet" href="/static/todomvc-app-css/index.css"/>
<title>"Leptos • TodoMVC"</title>
</head>
<body>
{}
</body>
<script type="module">import init, {{ main }} from './pkg/todomvc_ssr_client.js'; init().then(main);</script>
</html>"#,
run_scope({
|cx| {
let todos = Todos(vec![
Todo::new(cx, 0, "Buy milk".to_string()),
Todo::new(cx, 1, "???".to_string()),
Todo::new(cx, 2, "Profit!".to_string()),
]);
view! { cx,
<TodoMVC todos=todos/>
}
}
})
))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.service(render_todomvc)
.service(Files::new("/static", "../../todomvc/node_modules"))
.service(Files::new("/pkg", "../todomvc-ssr-client/pkg"))
.wrap(middleware::Compress::default())
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}

View File

@@ -5,15 +5,16 @@ edition = "2021"
[dependencies]
leptos = { path = "../../leptos", default-features = false }
miniserde = "0.1"
log = "0.4"
console_log = "0.2"
console_error_panic_hook = "0.1.7"
uuid = { version = "1", features = ["v4", "js", "serde"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[dev-dependencies]
wasm-bindgen-test = "0.3.0"
[features]
default = ["csr"]
csr = ["leptos/csr"]

View File

@@ -1,6 +1,6 @@
use leptos::{web_sys::HtmlInputElement, *};
use miniserde::json;
use storage::TodoSerialized;
use uuid::Uuid;
mod storage;
@@ -9,6 +9,7 @@ pub struct Todos(pub Vec<Todo>);
const STORAGE_KEY: &str = "todos-leptos";
// Basic operations to manipulate the todo list: nothing really interesting here
impl Todos {
pub fn new(cx: Scope) -> Self {
let starting_todos = if is_server!() {
@@ -18,7 +19,7 @@ impl Todos {
.get_item(STORAGE_KEY)
.ok()
.flatten()
.and_then(|value| json::from_str::<Vec<TodoSerialized>>(&value).ok())
.and_then(|value| serde_json::from_str::<Vec<TodoSerialized>>(&value).ok())
.map(|values| {
values
.into_iter()
@@ -40,31 +41,33 @@ impl Todos {
self.0.push(todo);
}
pub fn remove(&mut self, id: usize) {
pub fn remove(&mut self, id: Uuid) {
self.0.retain(|todo| todo.id != id);
}
pub fn remaining(&self) -> usize {
self.0.iter().filter(|todo| !(todo.completed)()).count()
// `todo.completed` is a signal, so we call .get() to access its value
self.0.iter().filter(|todo| !todo.completed.get()).count()
}
pub fn completed(&self) -> usize {
self.0.iter().filter(|todo| (todo.completed)()).count()
// `todo.completed` is a signal, so we call .get() to access its value
self.0.iter().filter(|todo| todo.completed.get()).count()
}
pub fn toggle_all(&self) {
// if all are complete, mark them all active instead
// if all are complete, mark them all active
if self.remaining() == 0 {
for todo in &self.0 {
if todo.completed.get() {
(todo.set_completed)(false);
todo.completed.set(false);
}
}
}
// otherwise, mark them all complete
else {
for todo in &self.0 {
(todo.set_completed)(true);
todo.completed.set(true);
}
}
}
@@ -76,33 +79,35 @@ impl Todos {
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Todo {
pub id: usize,
pub title: ReadSignal<String>,
pub set_title: WriteSignal<String>,
pub completed: ReadSignal<bool>,
pub set_completed: WriteSignal<bool>,
pub id: Uuid,
pub title: RwSignal<String>,
pub completed: RwSignal<bool>,
}
impl Todo {
pub fn new(cx: Scope, id: usize, title: String) -> Self {
pub fn new(cx: Scope, id: Uuid, title: String) -> Self {
Self::new_with_completed(cx, id, title, false)
}
pub fn new_with_completed(cx: Scope, id: usize, title: String, completed: bool) -> Self {
let (title, set_title) = create_signal(cx, title);
let (completed, set_completed) = create_signal(cx, completed);
pub fn new_with_completed(cx: Scope, id: Uuid, title: String, completed: bool) -> Self {
// RwSignal combines the getter and setter in one struct, rather than separating
// the getter from the setter. This makes it more convenient in some cases, such
// as when we're putting the signals into a struct and passing it around. There's
// no real difference: you could use `create_signal` here, or use `create_rw_signal`
// everywhere.
let title = create_rw_signal(cx, title);
let completed = create_rw_signal(cx, completed);
Self {
id,
title,
set_title,
completed,
set_completed,
}
}
pub fn toggle(&self) {
self.set_completed
.update(|completed| *completed = !*completed);
// A signal's `update()` function gives you a mutable reference to the current value
// You can use that to modify the value in place, which will notify any subscribers.
self.completed.update(|completed| *completed = !*completed);
}
}
@@ -110,24 +115,25 @@ const ESCAPE_KEY: u32 = 27;
const ENTER_KEY: u32 = 13;
#[component]
pub fn TodoMVC(cx: Scope, todos: Todos) -> Element {
let mut next_id = todos
.0
.iter()
.map(|todo| todo.id)
.max()
.map(|last| last + 1)
.unwrap_or(0);
pub fn TodoMVC(cx: Scope) -> Element {
// The `todos` are a signal, since we need to reactively update the list
let (todos, set_todos) = create_signal(cx, Todos::new(cx));
let (todos, set_todos) = create_signal(cx, todos);
// We provide a context that each <Todo/> component can use to update the list
// Here, I'm just passing the `WriteSignal`; a <Todo/> doesn't need to read the whole list
// (and shouldn't try to, as that would cause each individual <Todo/> to re-render when
// a new todo is added! This kind of hygiene is why `create_signal` defaults to read-write
// segregation.)
provide_context(cx, set_todos);
// Handle the three filter modes: All, Active, and Completed
let (mode, set_mode) = create_signal(cx, Mode::All);
window_event_listener("hashchange", move |_| {
let new_mode = location_hash().map(|hash| route(&hash)).unwrap_or_default();
set_mode(new_mode);
});
// Callback to add a todo on pressing the `Enter` key, if the field isn't empty
let add_todo = move |ev: web_sys::Event| {
let target = event_target::<HtmlInputElement>(&ev);
ev.stop_propagation();
@@ -136,15 +142,16 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Element {
let title = event_target_value(&ev);
let title = title.trim();
if !title.is_empty() {
let new = Todo::new(cx, next_id, title.to_string());
let new = Todo::new(cx, Uuid::new_v4(), title.to_string());
set_todos.update(|t| t.add(new));
next_id += 1;
target.set_value("");
}
}
};
let filtered_todos = create_memo::<Vec<Todo>>(cx, move |_| {
// A derived signal that filters the list of the todos depending on the filter mode
// This doesn't need to be a `Memo`, because we're only reading it in one place
let filtered_todos = move || {
todos.with(|todos| match mode.get() {
Mode::All => todos.0.to_vec(),
Mode::Active => todos
@@ -160,10 +167,15 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Element {
.cloned()
.collect(),
})
});
};
// effect to serialize to JSON
// this does reactive reads, so it will automatically serialize on any relevant change
// Serialization
//
// the effect reads the `todos` signal, and each `Todo`'s title and completed
// status, so it will automatically re-run on any change to the list of tasks
//
// this is the main point of `create_effect`: to synchronize reactive state
// with something outside the reactive system (like localStorage)
create_effect(cx, move |_| {
if let Ok(Some(storage)) = window().local_storage() {
let objs = todos
@@ -172,7 +184,7 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Element {
.iter()
.map(TodoSerialized::from)
.collect::<Vec<_>>();
let json = json::to_string(&objs);
let json = serde_json::to_string(&objs).expect("couldn't serialize Todos");
if storage.set_item(STORAGE_KEY, &json).is_err() {
log::error!("error while trying to set item in localStorage");
}
@@ -184,9 +196,17 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Element {
<section class="todoapp">
<header class="header">
<h1>"todos"</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus on:keydown=add_todo />
<input
class="new-todo"
placeholder="What needs to be done?"
autofocus
on:keydown=add_todo
/>
</header>
<section class="main" class:hidden={move || todos.with(|t| t.is_empty())}>
<section
class="main"
class:hidden={move || todos.with(|t| t.is_empty())}
>
<input id="toggle-all" class="toggle-all" type="checkbox"
prop:checked={move || todos.with(|t| t.remaining() > 0)}
on:input=move |_| set_todos.update(|t| t.toggle_all())
@@ -198,7 +218,10 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Element {
</For>
</ul>
</section>
<footer class="footer" class:hidden={move || todos.with(|t| t.is_empty())}>
<footer
class="footer"
class:hidden={move || todos.with(|t| t.is_empty())}
>
<span class="todo-count">
<strong>{move || todos.with(|t| t.remaining().to_string())}</strong>
{move || if todos.with(|t| t.remaining()) == 1 {
@@ -235,6 +258,8 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Element {
pub fn Todo(cx: Scope, todo: Todo) -> Element {
let (editing, set_editing) = create_signal(cx, false);
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
// this will be filled by _ref=input below
let input: Element;
let save = move |value: &str| {
@@ -242,16 +267,19 @@ pub fn Todo(cx: Scope, todo: Todo) -> Element {
if value.is_empty() {
set_todos.update(|t| t.remove(todo.id));
} else {
(todo.set_title)(value.to_string());
todo.title.set(value.to_string());
}
set_editing(false);
};
// the `input` variable above is filled by a ref, when the template is created
// so we create the template and store it in a variable here, so we can
// set up an effect using the `input` below
let tpl = view! { cx,
<li
class="todo"
class:editing={editing}
class:completed={move || (todo.completed)()}
class:completed={move || todo.completed.get()}
_ref=input
>
<div class="view">
@@ -261,7 +289,7 @@ pub fn Todo(cx: Scope, todo: Todo) -> Element {
prop:checked={move || (todo.completed)()}
on:input={move |ev| {
let checked = event_target_checked(&ev);
(todo.set_completed)(checked);
todo.completed.set(checked);
}}
/>
<label on:dblclick=move |_| set_editing(true)>
@@ -289,10 +317,13 @@ pub fn Todo(cx: Scope, todo: Todo) -> Element {
</li>
};
// toggling to edit mode should focus the input
#[cfg(any(feature = "csr", feature = "hydrate"))]
create_effect(cx, move |_| {
if editing() {
_ = input.unchecked_ref::<HtmlInputElement>().focus();
if let Some(input) = input.dyn_ref::<HtmlInputElement>() {
input.focus();
}
}
});

View File

@@ -4,5 +4,5 @@ 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, <TodoMVC todos=Todos::new(cx)/> })
mount_to_body(|cx| view! { cx, <TodoMVC/> })
}

View File

@@ -1,10 +1,11 @@
use crate::Todo;
use leptos::Scope;
use miniserde::{Deserialize, Serialize};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Serialize, Deserialize)]
pub struct TodoSerialized {
pub id: usize,
pub id: Uuid,
pub title: String,
pub completed: bool,
}
@@ -20,7 +21,7 @@ impl From<&Todo> for TodoSerialized {
Self {
id: todo.id,
title: todo.title.get(),
completed: (todo.completed)(),
completed: todo.completed.get(),
}
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos"
version = "0.0.11"
version = "0.0.15"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@@ -9,11 +9,11 @@ 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.11" }
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.11" }
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.11" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.11" }
leptos_server = { path = "../leptos_server", default-features = false, version = "0.0.11" }
leptos_core = { path = "../leptos_core", default-features = false, version = "0.0.13" }
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.12" }
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.13" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.12" }
leptos_server = { path = "../leptos_server", default-features = false, version = "0.0.15" }
[features]
default = ["csr", "serde"]

View File

@@ -53,10 +53,11 @@
//!
//! Here are links to the most important sections of the docs:
//! - **Reactivity**: the [leptos_reactive] overview, and more details in
//! - [create_signal], [ReadSignal], and [WriteSignal] (and [create_rw_signal] and [RwSignal])
//! - [create_memo] and [Memo]
//! - [create_resource] and [Resource]
//! - [create_effect]
//! - signals: [create_signal], [ReadSignal], and [WriteSignal] (and [create_rw_signal] and [RwSignal])
//! - computations: [create_memo] and [Memo]
//! - `async` interop: [create_resource] and [Resource] for loading data using `async` functions,
//! and [create_action] and [Action] to mutate data or imperatively call `async` functions.
//! - reactions: [create_effect]
//! - **Templating/Views**: the [view] macro
//! - **Routing**: the [leptos_router](https://docs.rs/leptos_router/latest/leptos_router/) crate
//!

View File

@@ -81,3 +81,33 @@ fn test_classes() {
);
});
}
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
#[test]
fn test_dash_prefixes() {
use leptos_dom::*;
use leptos_macro::view;
use leptos_reactive::{create_signal, run_scope};
let colons = run_scope(|cx| {
let (value, set_value) = create_signal(cx, 5);
view! {
cx,
<div class="my big" class:a={move || value() > 10} class:red=true class:car={move || value() > 1} attr:id="id"></div>
}
});
let dashes = run_scope(|cx| {
let (value, set_value) = create_signal(cx, 5);
view! {
cx,
<div class="my big" class-a={move || value() > 10} class-red=true class-car={move || value() > 1} attr-id="id"></div>
}
});
assert_eq!(colons, dashes);
assert_eq!(
dashes,
"<div data-hk=\"0-0\" class=\"my big red car\" id=\"id\"></div>"
);
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_core"
version = "0.0.11"
version = "0.0.13"
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.11" }
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.11" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.11" }
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.12" }
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.13" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.12" }
log = "0.4"
[features]

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_dom"
version = "0.0.11"
version = "0.0.12"
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.11" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.12" }
serde_json = "1"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4.31"

View File

@@ -75,17 +75,13 @@ pub fn insert_before(
debug_warn!("insert_before: trying to insert on a parent node that is not an element");
new.clone()
} else if let Some(existing) = existing {
if existing.parent_node().as_ref() == Some(parent.unchecked_ref()) {
match parent.insert_before(new, Some(existing)) {
Ok(c) => c,
Err(e) => {
debug_warn!("{:?}", e.as_string());
new.clone()
}
let parent = existing.parent_node().unwrap_throw();
match parent.insert_before(new, Some(existing)) {
Ok(c) => c,
Err(e) => {
debug_warn!("{:?}", e.as_string());
new.clone()
}
} else {
debug_warn!("insert_before: existing node is not a child of parent node");
parent.append_child(new).unwrap_throw()
}
} else {
parent.append_child(new).unwrap_throw()

View File

@@ -233,11 +233,7 @@ pub fn insert_expression(
}
Child::Null => match before {
Marker::BeforeChild(before) => {
if before.is_connected() {
Child::Node(insert_before(&parent, node, Some(before)))
} else {
Child::Node(append_child(&parent, node))
}
Child::Node(insert_before(&parent, node, Some(before)))
}
_ => Child::Node(append_child(&parent, node)),
},

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_macro"
version = "0.0.11"
version = "0.0.13"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@@ -15,10 +15,10 @@ proc-macro-error = "1"
proc-macro2 = "1"
quote = "1"
syn = { version = "1", features = ["full", "parsing", "extra-traits"] }
syn-rsx = "0.8.1"
syn-rsx = "0.8"
uuid = { version = "1", features = ["v4"] }
leptos_dom = { path = "../leptos_dom", version = "0.0.11" }
leptos_reactive = { path = "../leptos_reactive", version = "0.0.11" }
leptos_dom = { path = "../leptos_dom", version = "0.0.12" }
leptos_reactive = { path = "../leptos_reactive", version = "0.0.12" }
[dev-dependencies]
log = "0.4"

View File

@@ -83,9 +83,13 @@ mod server;
/// ```
///
/// 4. Dynamic content can be wrapped in curly braces (`{ }`) to insert text nodes, elements, or set attributes.
/// If you insert signal here, Leptos will create an effect to update the DOM whenever the value changes.
/// If you insert a signal here, Leptos will create an effect to update the DOM whenever the value changes.
/// *(“Signal” here means `Fn() -> T` where `T` is the appropriate type for that node: a `String` in case
/// of text nodes, a `bool` for `class:` attributes, etc.)*
///
/// Attributes can take a wide variety of primitive types that can be converted to strings. They can also
/// take an `Option`, in which case `Some` sets the attribute and `None` removes the attribute.
///
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast; use leptos_dom as leptos; use leptos_dom::Marker;
/// # run_scope(|cx| {
@@ -105,7 +109,7 @@ mod server;
/// # });
/// ```
///
/// 5. Event handlers can be added with `on:` attributes
/// 5. Event handlers can be added with `on:` attributes. If the event name contains a dash, you should use `on-` as the prefix instead.
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # run_scope(|cx| {
@@ -124,7 +128,10 @@ mod server;
/// ```
///
/// 6. DOM properties can be set with `prop:` attributes, which take any primitive type or `JsValue` (or a signal
/// that returns a primitive or JsValue).
/// that returns a primitive or JsValue). They can also take an `Option`, in which case `Some` sets the property
/// and `None` deletes the property.
///
/// If your property name contains a dash, you should use `prop-` as the prefix instead.
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # run_scope(|cx| {
@@ -147,6 +154,7 @@ mod server;
/// ```
///
/// 7. Classes can be toggled with `class:` attributes, which take a `bool` (or a signal that returns a `bool`).
/// If your class name contains a dash, you should use `class-` as the prefix instead.
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # run_scope(|cx| {

View File

@@ -115,7 +115,7 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
}
fn from_form_data(data: &[u8]) -> Result<Self, ServerFnError> {
let data = ::leptos::leptos_server::form_urlencoded::parse(data).collect::<Vec<_>>();
let data = ::leptos::form_urlencoded::parse(data).collect::<Vec<_>>();
Ok(Self {
#(#from_form_data_fields),*
})
@@ -129,8 +129,8 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
#[cfg(not(feature = "ssr"))]
fn call_fn_client(self) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Output, ::leptos::ServerFnError>>>> {
let #struct_name { #(#field_names_3),* } = self;
Box::pin(async move { #fn_name( #(#field_names_4),*).await })
let #struct_name { #(#field_names_3),* } = self;
Box::pin(async move { #fn_name( #(#field_names_4),*).await })
}
}
@@ -207,97 +207,4 @@ impl Parse for ServerFnBody {
attrs,
})
}
}
/*
/// Serialize the same way, regardless of flavor
impl ToTokens for ServerFnBody {
fn to_tokens(&self, out_tokens: &mut TokenStream2) {
let Self {
vis,
ident,
generics,
inputs,
output,
where_clause,
block,
attrs,
..
} = self;
let fields = inputs.iter().map(|f| {
let typed_arg = match f {
FnArg::Receiver(_) => todo!(),
FnArg::Typed(t) => t,
};
if let Type::Path(pat) = &*typed_arg.ty {
if pat.path.segments[0].ident == "Option" {
quote! {
#[builder(default, setter(strip_option))]
#vis #f
}
} else {
quote! { #vis #f }
}
} else {
quote! { #vis #f }
}
});
let struct_name = Ident::new(&format!("{}Props", ident), Span::call_site());
let field_names = inputs.iter().filter_map(|f| match f {
FnArg::Receiver(_) => todo!(),
FnArg::Typed(t) => Some(&t.pat),
});
let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
Some(lt)
} else {
None
};
out_tokens.append_all(quote! {
#[derive(Copy, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize, Debug)]
pub struct #struct_name {}
#[async_trait]
impl ServerFn for #struct_name {
type Output = i32;
fn url() -> &'static str {
"get_server_count"
}
fn as_form_data(&self) -> Vec<(&'static str, String)> {
vec![]
}
#[cfg(feature = "ssr")]
async fn call_fn(self) -> Result<Self::Output, ServerFnError> {
get_server_count().await
}
}
#[cfg(feature = "ssr")]
pub async fn get_server_count() -> Result<i32, ServerFnError> {
Ok(COUNT.load(Ordering::Relaxed))
}
#[cfg(not(feature = "ssr"))]
pub async fn get_server_count() -> Result<i32, ServerFnError> {
call_server_fn(#struct_name::url(), #struct_name {}).await
}
#[cfg(not(feature = "ssr"))]
pub async fn get_server_count_helper(args: #struct_name) -> Result<i32, ServerFnError> {
call_server_fn(#struct_name::url(), args).await
}
});
}
}
/*
*/
*/
}

View File

@@ -152,7 +152,7 @@ fn root_element_to_tokens(
#[derive(Clone, Debug)]
enum PrevSibChange {
Sib(Ident),
//Parent,
Parent,
Skip,
}
@@ -214,8 +214,14 @@ fn element_to_tokens(
.iter()
.filter_map(|node| {
node.name_as_string().and_then(|name| {
if name.starts_with("class:") {
let name = name.replacen("class:", "", 1);
if name.starts_with("class:") || name.starts_with("class-") {
let name = if name.starts_with("class:") {
name.replacen("class:", "", 1)
} else if name.starts_with("class-") {
name.replacen("class-", "", 1)
} else {
name
};
let value = node.value.as_ref().expect("class: attributes need values");
let span = node.name_span().expect("missing span for class name");
Some(quote_spanned! {
@@ -270,7 +276,7 @@ fn element_to_tokens(
quote_spanned! {
span => let #this_el_ident = #debug_name;
let #this_el_ident = #parent.clone().unchecked_into::<web_sys::Node>();
//log::debug!("=> got {}", #this_el_ident.node_name());
//debug!("=> got {}", #this_el_ident.node_name());
}
} else if let Some(prev_sib) = &prev_sib {
quote_spanned! {
@@ -345,11 +351,12 @@ fn element_to_tokens(
expressions,
multi,
mode,
idx == 0
);
prev_sib = match curr_id {
PrevSibChange::Sib(id) => Some(id),
//PrevSibChange::Parent => None,
PrevSibChange::Parent => None,
PrevSibChange::Skip => prev_sib,
};
}
@@ -400,6 +407,13 @@ fn attr_to_tokens(
} else {
name
};
let name = if name.starts_with("attr:") {
name.replacen("attr:", "", 1)
} else if name.starts_with("attr-") {
name.replacen("attr-", "", 1)
} else {
name
};
let value = match &node.value {
Some(expr) => match expr {
syn::Expr::Lit(expr_lit) => {
@@ -456,16 +470,20 @@ fn attr_to_tokens(
}
}
// Event Handlers
else if name.starts_with("on:") {
else if name.starts_with("on:") || name.starts_with("on-") {
let handler = node
.value
.as_ref()
.expect("event listener attributes need a value");
if mode != Mode::Ssr {
let event_name = name.replacen("on:", "", 1);
let name = if name.starts_with("on:") {
name.replacen("on:", "", 1)
} else {
name.replacen("on-", "", 1)
};
expressions.push(quote_spanned! {
span => add_event_listener(#el_id.unchecked_ref(), #event_name, #handler);
span => add_event_listener(#el_id.unchecked_ref(), #name, #handler);
});
} else {
// this is here to avoid warnings about unused signals
@@ -476,10 +494,14 @@ fn attr_to_tokens(
}
}
// Properties
else if name.starts_with("prop:") {
else if name.starts_with("prop:") || name.starts_with("prop-") {
let name = if name.starts_with("prop:") {
name.replacen("prop:", "", 1)
} else {
name.replacen("prop-", "", 1)
};
// can't set properties in SSR
if mode != Mode::Ssr {
let name = name.replacen("prop:", "", 1);
if mode != Mode::Ssr {
let value = node.value.as_ref().expect("prop: blocks need values");
expressions.push(quote_spanned! {
span => leptos_dom::property(#cx, #el_id.unchecked_ref(), #name, #value.into_property(#cx))
@@ -487,11 +509,15 @@ fn attr_to_tokens(
}
}
// Classes
else if name.starts_with("class:") {
else if name.starts_with("class:") || name.starts_with("class-") {
let name = if name.starts_with("class:") {
name.replacen("class:", "", 1)
} else {
name.replacen("class-", "", 1)
};
if mode == Mode::Ssr {
// handled separately because they need to be merged
} else {
let name = name.replacen("class:", "", 1);
let value = node.value.as_ref().expect("class: attributes need values");
expressions.push(quote_spanned! {
span => leptos_dom::class(#cx, #el_id.unchecked_ref(), #name, #value.into_class(#cx))
@@ -571,6 +597,7 @@ fn child_to_tokens(
expressions: &mut Vec<TokenStream>,
multi: bool,
mode: Mode,
is_first_child: bool
) -> PrevSibChange {
match node.node_type {
NodeType::Element => {
@@ -588,6 +615,7 @@ fn child_to_tokens(
next_co_id,
multi,
mode,
is_first_child
)
} else {
PrevSibChange::Sib(element_to_tokens(
@@ -744,6 +772,7 @@ fn component_to_tokens(
next_co_id: &mut usize,
multi: bool,
mode: Mode,
is_first_child: bool
) -> PrevSibChange {
let create_component = create_component(cx, node, mode);
let span = node.name_span().unwrap();
@@ -831,7 +860,11 @@ fn component_to_tokens(
match current {
Some(el) => PrevSibChange::Sib(el),
None => PrevSibChange::Skip,
None => if is_first_child {
PrevSibChange::Parent
} else {
PrevSibChange::Skip
},
}
}
@@ -875,9 +908,13 @@ fn create_component(cx: &Ident, node: &Node, mode: Mode) -> TokenStream {
let props = node.attributes.iter().filter_map(|attr| {
let attr_name = attr.name_as_string().unwrap_or_default();
if attr_name.starts_with("on:")
|| attr_name.starts_with("on-")
|| attr_name.starts_with("prop:")
|| attr_name.starts_with("prop-")
|| attr_name.starts_with("class:")
|| attr_name.starts_with("class-")
|| attr_name.starts_with("attr:")
|| attr_name.starts_with("attr-")
{
None
} else {
@@ -896,12 +933,23 @@ fn create_component(cx: &Ident, node: &Node, mode: Mode) -> TokenStream {
let mut other_attrs = node.attributes.iter().filter_map(|attr| {
let attr_name = attr.name_as_string().unwrap_or_default();
// Event Listeners
if let Some(event_name) = attr_name.strip_prefix("on:") {
let span = attr.name_span().unwrap();
let handler = attr
.value
.as_ref()
.expect("event listener attributes need a value");
.expect("on: event listener attributes need a value");
Some(quote_spanned! {
span => add_event_listener(#component_name.unchecked_ref(), #event_name, #handler)
})
}
else if let Some(event_name) = attr_name.strip_prefix("on-") {
let span = attr.name_span().unwrap();
let handler = attr
.value
.as_ref()
.expect("on- event listener attributes need a value");
Some(quote_spanned! {
span => add_event_listener(#component_name.unchecked_ref(), #event_name, #handler)
})
@@ -913,6 +961,12 @@ fn create_component(cx: &Ident, node: &Node, mode: Mode) -> TokenStream {
span => leptos_dom::property(#cx, #component_name.unchecked_ref(), #name, #value.into_property(#cx))
})
}
else if let Some(name) = attr_name.strip_prefix("prop-") {
let value = attr.value.as_ref().expect("prop- attributes need values");
Some(quote_spanned! {
span => leptos_dom::property(#cx, #component_name.unchecked_ref(), #name, #value.into_property(#cx))
})
}
// Classes
else if let Some(name) = attr_name.strip_prefix("class:") {
let value = attr.value.as_ref().expect("class: attributes need values");
@@ -920,10 +974,21 @@ fn create_component(cx: &Ident, node: &Node, mode: Mode) -> TokenStream {
span => leptos_dom::class(#cx, #component_name.unchecked_ref(), #name, #value.into_class(#cx))
})
}
else if let Some(name) = attr_name.strip_prefix("class-") {
let value = attr.value.as_ref().expect("class: attributes need values");
Some(quote_spanned! {
span => leptos_dom::class(#cx, #component_name.unchecked_ref(), #name, #value.into_class(#cx))
})
}
// Attributes
else if let Some(name) = attr_name.strip_prefix("attr:") {
let value = attr.value.as_ref().expect("attr: attributes need values");
let name = name.replace('_', "-");
Some(quote_spanned! {
span => leptos_dom::attribute(#cx, #component_name.unchecked_ref(), #name, #value.into_attribute(#cx))
})
}
else if let Some(name) = attr_name.strip_prefix("attr-") {
let value = attr.value.as_ref().expect("attr- attributes need values");
Some(quote_spanned! {
span => leptos_dom::attribute(#cx, #component_name.unchecked_ref(), #name, #value.into_attribute(#cx))
})

View File

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

View File

@@ -1,11 +1,15 @@
[package]
name = "leptos_server"
version = "0.0.11"
version = "0.0.15"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
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.11" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.11" }
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.12" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.12" }
form_urlencoded = "1"
gloo-net = "0.2"
lazy_static = "1"
@@ -13,11 +17,35 @@ linear-map = "1"
serde = { version = "1", features = ["derive"] }
thiserror = "1"
[dev-dependencies]
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0" }
leptos = { path = "../leptos", default-features = false, version = "0.0" }
[features]
csr = ["leptos_dom/csr", "leptos_reactive/csr"]
hydrate = ["leptos_dom/hydrate", "leptos_reactive/hydrate"]
ssr = ["leptos_dom/ssr", "leptos_reactive/ssr"]
stable = ["leptos_dom/stable", "leptos_reactive/stable"]
csr = [
"leptos_dom/csr",
"leptos_reactive/csr",
"leptos_macro/csr",
"leptos/csr",
]
hydrate = [
"leptos_dom/hydrate",
"leptos_reactive/hydrate",
"leptos_macro/hydrate",
"leptos/hydrate",
]
ssr = [
"leptos_dom/ssr",
"leptos_reactive/ssr",
"leptos_macro/ssr",
"leptos/csr",
]
stable = [
"leptos_dom/stable",
"leptos_reactive/stable",
"leptos_macro/stable",
"leptos/stable",
]
[package.metadata.cargo-all-features]
denylist = ["stable"]

View File

@@ -56,16 +56,6 @@
//! - **Server function arguments and return types must be [Serializable](leptos_reactive::Serializable).**
//! This should be fairly obvious: we have to serialize arguments to send them to the server, and we
//! need to deserialize the result to return it to the client.
//!
//! ### `create_action`
//!
//! The easiest way to call server functions from the client is the `create_action` primitive.
//! This returns an [Action](crate::Action), with a [dispatch](crate::Action::dispatch) method
//! that can run any `async` function, including one that contains one or more calls to server functions.
//!
//! Dispatching an action increments its [version](crate::Action::version) field, which is a
//! signal. This is very useful, as it can be used to invalidate a [Resource](leptos_reactive::Resource)
//! that reads from the same data.
pub use form_urlencoded;
use leptos_reactive::*;
@@ -73,22 +63,65 @@ use serde::{Deserialize, Serialize};
use std::{future::Future, pin::Pin, rc::Rc};
use thiserror::Error;
#[cfg(feature = "ssr")]
#[cfg(any(feature = "ssr", doc))]
use std::{
collections::HashMap,
sync::{Arc, RwLock},
};
#[cfg(feature = "ssr")]
#[cfg(any(feature = "ssr", doc))]
type ServerFnTraitObj =
dyn Fn(&[u8]) -> Pin<Box<dyn Future<Output = Result<String, ServerFnError>>>> + Send + Sync;
#[cfg(feature = "ssr")]
#[cfg(any(feature = "ssr", doc))]
lazy_static::lazy_static! {
pub static ref REGISTERED_SERVER_FUNCTIONS: Arc<RwLock<HashMap<&'static str, Arc<ServerFnTraitObj>>>> = Default::default();
static ref REGISTERED_SERVER_FUNCTIONS: Arc<RwLock<HashMap<&'static str, Arc<ServerFnTraitObj>>>> = Default::default();
}
#[cfg(feature = "ssr")]
/// Attempts to find a server function registered at the given path.
///
/// This can be used by a server to handle the requests, as in the following example (using `actix-web`)
///
/// ```rust, ignore
/// #[post("{tail:.*}")]
/// async fn handle_server_fns(
/// req: HttpRequest,
/// params: web::Path<String>,
/// body: web::Bytes,
/// ) -> impl Responder {
/// let path = params.into_inner();
/// let accept_header = req
/// .headers()
/// .get("Accept")
/// .and_then(|value| value.to_str().ok());
///
/// if let Some(server_fn) = server_fn_by_path(path.as_str()) {
/// let body: &[u8] = &body;
/// match server_fn(&body).await {
/// Ok(serialized) => {
/// // if this is Accept: application/json then send a serialized JSON response
/// if let Some("application/json") = accept_header {
/// HttpResponse::Ok().body(serialized)
/// }
/// // otherwise, it's probably a <form> submit or something: redirect back to the referrer
/// else {
/// HttpResponse::SeeOther()
/// .insert_header(("Location", "/"))
/// .content_type("application/json")
/// .body(serialized)
/// }
/// }
/// Err(e) => {
/// eprintln!("server function error: {e:#?}");
/// HttpResponse::InternalServerError().body(e.to_string())
/// }
/// }
/// } else {
/// HttpResponse::BadRequest().body(format!("Could not find a server function at that route."))
/// }
/// }
/// ```
#[cfg(any(feature = "ssr", doc))]
pub fn server_fn_by_path(path: &str) -> Option<Arc<ServerFnTraitObj>> {
REGISTERED_SERVER_FUNCTIONS
.read()
@@ -96,25 +129,43 @@ pub fn server_fn_by_path(path: &str) -> Option<Arc<ServerFnTraitObj>> {
.and_then(|fns| fns.get(path).cloned())
}
/// Defines a "server function." A server function can be called from the server or the client,
/// but the body of its code will only be run on the server, i.e., if a crate feature `ssr` is enabled.
///
/// (This follows the same convention as the Leptos framework's distinction between `ssr` for server-side rendering,
/// and `csr` and `hydrate` for client-side rendering and hydration, respectively.)
///
/// Server functions are created using the `server` macro.
///
/// The function should be registered by calling `ServerFn::register()`. The set of server functions
/// can be queried on the server for routing purposes by calling [server_fn_by_path].
///
/// Technically, the trait is implemented on a type that describes the server function's arguments.
pub trait ServerFn
where
Self: Sized + 'static,
{
type Output: Serializable;
/// The path at which the server function can be reached on the server.
fn url() -> &'static str;
/// A set of `(input_name, input_value)` pairs used to serialize the arguments to the server function.
fn as_form_data(&self) -> Vec<(&'static str, String)>;
/// Deserializes the arguments to the server function from form data.
fn from_form_data(data: &[u8]) -> Result<Self, ServerFnError>;
#[cfg(feature = "ssr")]
/// Runs the function on the server.
#[cfg(any(feature = "ssr", doc))]
fn call_fn(self) -> Pin<Box<dyn Future<Output = Result<Self::Output, ServerFnError>> + Send>>;
#[cfg(not(feature = "ssr"))]
/// Runs the function on the client by sending an HTTP request to the server.
#[cfg(any(not(feature = "ssr"), doc))]
fn call_fn_client(self) -> Pin<Box<dyn Future<Output = Result<Self::Output, ServerFnError>>>>;
#[cfg(feature = "ssr")]
/// Registers the server function, allowing the server to query it by URL.
#[cfg(any(feature = "ssr", doc))]
fn register() -> Result<(), ServerFnError> {
// create the handler for this server function
// takes a String -> returns its async value
@@ -156,6 +207,7 @@ where
}
}
/// Type for errors that can occur when using server functions.
#[derive(Error, Debug, Clone, Serialize, Deserialize)]
pub enum ServerFnError {
#[error("error while trying to register the server function: {0}")]
@@ -174,6 +226,7 @@ pub enum ServerFnError {
MissingArg(String),
}
/// Executes the HTTP call to call a server function from the client, given its URL and argument type.
#[cfg(not(feature = "ssr"))]
pub async fn call_server_fn<T>(url: &str, args: impl ServerFn) -> Result<T, ServerFnError>
where
@@ -213,12 +266,87 @@ where
T::from_json(&text).map_err(|e| ServerFnError::Deserialization(e.to_string()))
}
/// An action synchronizes an imperative `async` call to the synchronous reactive system.
///
/// If youre trying to load data by running an `async` function reactively, you probably
/// want to use a [Resource](leptos_reactive::Resource) instead. If youre trying to occasionally
/// run an `async` function in response to something like a user clicking a button, you're in the right place.
///
/// ```rust
/// # use leptos_reactive::run_scope;
/// # use leptos_server::create_action;
/// # run_scope(|cx| {
/// async fn send_new_todo_to_api(task: String) -> usize {
/// // do something...
/// // return a task id
/// 42
/// }
/// let save_data = create_action(cx, |task: &String| {
/// // `task` is given as `&String` because its value is available in `input`
/// send_new_todo_to_api(task.clone())
/// });
///
/// // the argument currently running
/// let input = save_data.input();
/// // the most recent returned result
/// let result_of_call = save_data.value();
/// // whether the call is pending
/// let pending = save_data.pending();
/// // how many times the action has run
/// // useful for reactively updating something else in response to a `dispatch` and response
/// let version = save_data.version;
///
/// // before we do anything
/// assert_eq!(input(), None); // no argument yet
/// assert_eq!(pending(), false); // isn't pending a response
/// assert_eq!(result_of_call(), None); // there's no "last value"
/// assert_eq!(version(), 0);
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// // dispatch the action
/// save_data.dispatch("My todo".to_string());
///
/// // when we're making the call
/// // assert_eq!(input(), Some("My todo".to_string()));
/// // assert_eq!(pending(), true); // is pending
/// // assert_eq!(result_of_call(), None); // has not yet gotten a response
///
/// // after call has resolved
/// assert_eq!(input(), None); // input clears out after resolved
/// assert_eq!(pending(), false); // no longer pending
/// assert_eq!(result_of_call(), Some(42));
/// assert_eq!(version(), 1);
/// # }
/// # });
/// ```
///
/// The input to the `async` function should always be a single value,
/// but it can be of any type. The argument is always passed by reference to the
/// function, because it is stored in [Action::input] as well.
///
/// ```rust
/// # use leptos_reactive::run_scope;
/// # use leptos_server::create_action;
/// # run_scope(|cx| {
/// // if there's a single argument, just use that
/// let action1 = create_action(cx, |input: &String| {
/// let input = input.clone();
/// async move { todo!() }
/// });
///
/// // if there are no arguments, use the unit type `()`
/// let action2 = create_action(cx, |input: &()| async { todo!() });
///
/// // if there are multiple arguments, use a tuple
/// let action3 = create_action(cx, |input: &(usize, String)| async { todo!() });
/// # });
/// ```
#[derive(Clone)]
pub struct Action<I, O>
where
I: 'static,
O: 'static,
{
/// How many times the action has successfully resolved.
pub version: RwSignal<usize>,
input: RwSignal<Option<I>>,
value: RwSignal<Option<O>>,
@@ -233,44 +361,129 @@ where
I: 'static,
O: 'static,
{
pub fn using_server_fn<T: ServerFn>(mut self) -> Self {
self.url = Some(T::url());
self
}
pub fn pending(&self) -> impl Fn() -> bool {
let value = self.value;
move || value.with(|val| val.is_some())
}
pub fn input(&self) -> ReadSignal<Option<I>> {
self.input.read_only()
}
pub fn value(&self) -> ReadSignal<Option<O>> {
self.value.read_only()
}
pub fn url(&self) -> Option<&str> {
self.url
}
/// Calls the server function a reference to the input type as its argument.
pub fn dispatch(&self, input: I) {
let fut = (self.action_fn)(&input);
self.input.set(Some(input));
let input = self.input;
let version = self.version;
let pending = self.pending;
let value = self.value;
pending.set(true);
spawn_local(async move {
let new_value = fut.await;
value.set(Some(new_value));
input.set(None);
pending.set(false);
value.set(Some(new_value));
version.update(|n| *n += 1);
})
}
/// Whether the action has been dispatched and is currently waiting for its future to be resolved.
pub fn pending(&self) -> ReadSignal<bool> {
self.pending.read_only()
}
/// The argument that was dispatched to the `async` function,
/// only while we are waiting for it to resolve.
pub fn input(&self) -> ReadSignal<Option<I>> {
self.input.read_only()
}
/// The most recent return value of the `async` function.
pub fn value(&self) -> ReadSignal<Option<O>> {
self.value.read_only()
}
/// The URL associated with the action (typically as part of a server function.)
/// This enables integration with the `ActionForm` component in `leptos_router`.
pub fn url(&self) -> Option<&str> {
self.url
}
/// Associates the URL of the given server function with this action.
/// This enables integration with the `ActionForm` component in `leptos_router`.
pub fn using_server_fn<T: ServerFn>(mut self) -> Self {
self.url = Some(T::url());
self
}
}
/// Creates an [Action] to synchronize an imperative `async` call to the synchronous reactive system.
///
/// If youre trying to load data by running an `async` function reactively, you probably
/// want to use a [create_resource](leptos_reactive::create_resource) instead. If youre trying
/// to occasionally run an `async` function in response to something like a user clicking a button,
/// you're in the right place.
///
/// ```rust
/// # use leptos_reactive::run_scope;
/// # use leptos_server::create_action;
/// # run_scope(|cx| {
/// async fn send_new_todo_to_api(task: String) -> usize {
/// // do something...
/// // return a task id
/// 42
/// }
/// let save_data = create_action(cx, |task: &String| {
/// // `task` is given as `&String` because its value is available in `input`
/// send_new_todo_to_api(task.clone())
/// });
///
/// // the argument currently running
/// let input = save_data.input();
/// // the most recent returned result
/// let result_of_call = save_data.value();
/// // whether the call is pending
/// let pending = save_data.pending();
/// // how many times the action has run
/// // useful for reactively updating something else in response to a `dispatch` and response
/// let version = save_data.version;
///
/// // before we do anything
/// assert_eq!(input(), None); // no argument yet
/// assert_eq!(pending(), false); // isn't pending a response
/// assert_eq!(result_of_call(), None); // there's no "last value"
/// assert_eq!(version(), 0);
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// // dispatch the action
/// save_data.dispatch("My todo".to_string());
///
/// // when we're making the call
/// // assert_eq!(input(), Some("My todo".to_string()));
/// // assert_eq!(pending(), true); // is pending
/// // assert_eq!(result_of_call(), None); // has not yet gotten a response
///
/// // after call has resolved
/// assert_eq!(input(), None); // input clears out after resolved
/// assert_eq!(pending(), false); // no longer pending
/// assert_eq!(result_of_call(), Some(42));
/// assert_eq!(version(), 1);
/// # }
/// # });
/// ```
///
/// The input to the `async` function should always be a single value,
/// but it can be of any type. The argument is always passed by reference to the
/// function, because it is stored in [Action::input] as well.
///
/// ```rust
/// # use leptos_reactive::run_scope;
/// # use leptos_server::create_action;
/// # run_scope(|cx| {
/// // if there's a single argument, just use that
/// let action1 = create_action(cx, |input: &String| {
/// let input = input.clone();
/// async move { todo!() }
/// });
///
/// // if there are no arguments, use the unit type `()`
/// let action2 = create_action(cx, |input: &()| async { todo!() });
///
/// // if there are multiple arguments, use a tuple
/// let action3 = create_action(cx, |input: &(usize, String)| async { todo!() });
/// # });
/// ```
pub fn create_action<I, O, F, Fu>(cx: Scope, action_fn: F) -> Action<I, O>
where
I: 'static,
@@ -297,6 +510,22 @@ where
}
}
/// Creates an [Action] that can be used to call a server function.
///
/// ```rust
/// # use leptos_reactive::run_scope;
/// # use leptos_server::{create_server_action, ServerFnError, ServerFn};
/// # use leptos_macro::server;
///
/// #[server(MyServerFn)]
/// async fn my_server_fn() -> Result<(), ServerFnError> {
/// todo!()
/// }
///
/// # run_scope(|cx| {
/// let my_server_action = create_server_action::<MyServerFn>(cx);
/// # });
/// ```
pub fn create_server_action<S>(cx: Scope) -> Action<S, Result<S::Output, ServerFnError>>
where
S: Clone + ServerFn,

View File

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

View File

@@ -6,23 +6,39 @@ use wasm_bindgen::JsCast;
use crate::{use_navigate, use_resolved_path, ToHref};
/// Properties that can be passed to the [Form] component, which is an HTML
/// [`form`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)
/// progressively enhanced to use client-side routing.
#[derive(TypedBuilder)]
pub struct FormProps<A>
where
A: ToHref + '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`).
#[builder(default, setter(strip_option))]
method: Option<&'static str>,
action: A,
pub method: Option<&'static str>,
/// [`action`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-action)
/// is the URL that processes the form submission. Takes a [String], [&str], or a reactive
/// function that returns a [String].
pub action: A,
/// [`enctype`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-enctype)
/// is the MIME type of the form submission if `method` is `post`.
#[builder(default, setter(strip_option))]
enctype: Option<String>,
children: Box<dyn Fn() -> Vec<Element>>,
pub enctype: Option<String>,
/// A signal that will be incremented whenever the form is submitted with `post`. This can useful
/// for reactively updating a [Resource] or another signal whenever the form has been submitted.
#[builder(default, setter(strip_option))]
version: Option<RwSignal<usize>>,
pub version: Option<RwSignal<usize>>,
/// A signal that will be set if the form submission ends in an error.
#[builder(default, setter(strip_option))]
error: Option<RwSignal<Option<Box<dyn Error>>>>,
pub error: Option<RwSignal<Option<Box<dyn Error>>>>,
/// Component children; should include the HTML of the form elements.
pub children: Box<dyn Fn() -> Vec<Element>>,
}
/// An HTML [`form`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form) progressively
/// enhanced to use client-side routing.
#[allow(non_snake_case)]
pub fn Form<A>(cx: Scope, props: FormProps<A>) -> Element
where
@@ -159,6 +175,9 @@ where
if let Some(version) = action_version {
version.update(|n| *n += 1);
}
if let Some(error) = error {
error.set(None);
}
if resp.status() == 303 {
if let Some(redirect_url) = resp.headers().get("Location") {
@@ -190,16 +209,27 @@ where
}
}
/// Properties that can be passed to the [ActionForm] component, which
/// automatically turns a server [Action](leptos_server::Action) into an HTML
/// [`form`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)
/// progressively enhanced to use client-side routing.
#[derive(TypedBuilder)]
pub struct ActionFormProps<I, O>
where
I: 'static,
O: 'static,
{
action: Action<I, O>,
children: Box<dyn Fn() -> Vec<Element>>,
/// The action from which to build the form. This should include a URL, which can be generated
/// by default using [create_server_action](leptos_server::create_server_action) or added
/// manually using [leptos_server::Action::using_server_fn].
pub action: Action<I, O>,
/// Component children; should include the HTML of the form elements.
pub children: Box<dyn Fn() -> Vec<Element>>,
}
/// Automatically turns a server [Action](leptos_server::Action) into an HTML
/// [`form`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)
/// progressively enhanced to use client-side routing.
#[allow(non_snake_case)]
pub fn ActionForm<I, O>(cx: Scope, props: ActionFormProps<I, O>) -> Element
where

View File

@@ -3,12 +3,16 @@ use leptos::leptos_dom::IntoChild;
use leptos::*;
use typed_builder::TypedBuilder;
#[cfg(not(feature = "ssr"))]
#[cfg(any(feature = "csr", feature = "hydrate"))]
use wasm_bindgen::JsCast;
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 {
/// 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 + '_>;
}
@@ -35,6 +39,9 @@ where
}
}
/// Properties that can be passed to the [A] component, which is an HTML
/// [`a`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a)
/// progressively enhanced to use client-side routing.
#[derive(TypedBuilder)]
pub struct AProps<C, H>
where
@@ -43,21 +50,24 @@ where
{
/// Used to calculate the link's `href` attribute. Will be resolved relative
/// to the current route.
href: H,
pub href: H,
/// If `true`, the link is marked active when the location matches exactly;
/// if false, link is marked active if the current route starts with it.
#[builder(default)]
exact: bool,
pub exact: bool,
/// An object of any type that will be pushed to router state
#[builder(default, setter(strip_option))]
state: Option<State>,
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)]
replace: bool,
children: Box<dyn Fn() -> Vec<C>>,
pub replace: bool,
/// 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.
#[allow(non_snake_case)]
pub fn A<C, H>(cx: Scope, props: AProps<C, H>) -> Element
where

View File

@@ -1,6 +1,8 @@
use crate::use_route;
use leptos::*;
/// Displays the child route nested in a parent route, allowing you to control exactly where
/// that child route is displayed. Renders nothing if there is no nested child.
#[component]
pub fn Outlet(cx: Scope) -> Child {
let route = use_route(cx);

View File

@@ -5,27 +5,33 @@ use typed_builder::TypedBuilder;
use crate::{
matching::{resolve_path, PathMatch, RouteDefinition, RouteMatch},
Action, Loader, ParamsMap, RouterContext,
ParamsMap, RouterContext,
};
pub struct ChildlessRoute {}
/// Properties that can be passed to a [Route] component, which describes
/// a portion of the nested layout of the app, specifying the route it should match,
/// the element it should display, and data that should be loaded alongside the route.
#[derive(TypedBuilder)]
pub struct RouteProps<E, F>
where
E: IntoChild,
F: Fn(Scope) -> E + 'static,
{
path: &'static str,
element: F,
/// The path fragment that this route should match. This can be static (`users`),
/// include a parameter (`:id`) or an optional parameter (`:id?`), or match a
/// wildcard (`user/*any`).
pub path: &'static str,
/// The view that should be shown when this route is matched. This can be any function
/// that takes a [Scope] and returns an [Element] (like `|cx| view! { cx, <p>"Show this"</p> })`
/// or `|cx| view! { cx, <MyComponent/>` } or even, for a component with no props, `MyComponent`).
pub element: F,
/// `children` may be empty or include nested routes.
#[builder(default, setter(strip_option))]
loader: Option<Loader>,
#[builder(default, setter(strip_option))]
action: Option<Action>,
#[builder(default, setter(strip_option))]
children: Option<Box<dyn Fn() -> Vec<RouteDefinition>>>,
pub children: Option<Box<dyn Fn() -> Vec<RouteDefinition>>>,
}
/// Describes a portion of the nested layout of the app, specifying the route it should match,
/// the element it should display, and data that should be loaded alongside the route.
#[allow(non_snake_case)]
pub fn Route<E, F>(_cx: Scope, props: RouteProps<E, F>) -> RouteDefinition
where
@@ -34,13 +40,12 @@ where
{
RouteDefinition {
path: props.path,
loader: props.loader,
action: props.action,
children: props.children.map(|c| c()).unwrap_or_default(),
element: Rc::new(move |cx| (props.element)(cx).into_child(cx)),
}
}
/// Context type that contains information about the current, matched route.
#[derive(Debug, Clone, PartialEq)]
pub struct RouteContext {
inner: Rc<RouteContextInner>,
@@ -57,12 +62,7 @@ impl RouteContext {
let base = base.path();
let RouteMatch { path_match, route } = matcher()?;
let PathMatch { path, .. } = path_match;
let RouteDefinition {
element,
loader,
action,
..
} = route.key;
let RouteDefinition { element, .. } = route.key;
let params = create_memo(cx, move |_| {
matcher()
.map(|matched| matched.path_match.params)
@@ -74,8 +74,6 @@ impl RouteContext {
cx,
base_path: base.to_string(),
child: Box::new(child),
loader,
action,
path,
original_path: route.original_path.to_string(),
params,
@@ -84,30 +82,27 @@ impl RouteContext {
})
}
/// Returns the reactive scope of the current route.
pub fn cx(&self) -> Scope {
self.inner.cx
}
/// Returns the URL path of the current route.
pub fn path(&self) -> &str {
&self.inner.path
}
/// A reactive wrapper for the route parameters that are currently matched.
pub fn params(&self) -> Memo<ParamsMap> {
self.inner.params
}
pub fn loader(&self) -> &Option<Loader> {
&self.inner.loader
}
pub fn base(cx: Scope, path: &str, fallback: Option<fn() -> Element>) -> Self {
pub(crate) fn base(cx: Scope, path: &str, fallback: Option<fn() -> Element>) -> Self {
Self {
inner: Rc::new(RouteContextInner {
cx,
base_path: path.to_string(),
child: Box::new(|| None),
loader: None,
action: None,
path: path.to_string(),
original_path: path.to_string(),
params: create_memo(cx, |_| ParamsMap::new()),
@@ -116,14 +111,17 @@ impl RouteContext {
}
}
/// Resolves a relative route, relative to the current route's path.
pub fn resolve_path<'a>(&'a self, to: &'a str) -> Option<Cow<'a, str>> {
resolve_path(&self.inner.base_path, to, Some(&self.inner.path))
}
/// The nested child route, if any.
pub fn child(&self) -> Option<RouteContext> {
(self.inner.child)()
}
/// The view associated with the current route.
pub fn outlet(&self) -> impl IntoChild {
(self.inner.outlet)()
}
@@ -133,8 +131,6 @@ pub(crate) struct RouteContextInner {
cx: Scope,
base_path: String,
pub(crate) child: Box<dyn Fn() -> Option<RouteContext>>,
pub(crate) loader: Option<Loader>,
pub(crate) action: Option<Action>,
pub(crate) path: String,
pub(crate) original_path: String,
pub(crate) params: Memo<ParamsMap>,
@@ -160,31 +156,3 @@ impl std::fmt::Debug for RouteContextInner {
.finish()
}
}
pub trait IntoChildRoutes {
fn into_child_routes(self) -> Vec<RouteDefinition>;
}
impl IntoChildRoutes for () {
fn into_child_routes(self) -> Vec<RouteDefinition> {
vec![]
}
}
impl IntoChildRoutes for RouteDefinition {
fn into_child_routes(self) -> Vec<RouteDefinition> {
vec![self]
}
}
impl IntoChildRoutes for Option<RouteDefinition> {
fn into_child_routes(self) -> Vec<RouteDefinition> {
self.map(|c| vec![c]).unwrap_or_default()
}
}
impl IntoChildRoutes for Vec<RouteDefinition> {
fn into_child_routes(self) -> Vec<RouteDefinition> {
self
}
}

View File

@@ -20,15 +20,23 @@ use crate::{
#[cfg(not(feature = "ssr"))]
use crate::{unescape, Url};
/// Props for the [Router] component, which sets up client-side and server-side routing.
#[derive(TypedBuilder)]
pub struct RouterProps {
/// The base URL for the router. Defaults to "".
#[builder(default, setter(strip_option))]
base: Option<&'static str>,
pub base: Option<&'static str>,
#[builder(default, setter(strip_option))]
fallback: Option<fn() -> Element>,
children: Box<dyn Fn() -> Vec<Element>>,
/// A fallback that should be shown if no route is matched.
pub fallback: Option<fn() -> Element>,
/// The `<Router/>` should usually wrap your whole page. It can contain
/// any elements, and should include a [Routes](crate::Routes) component somewhere
/// to define and display [Route](crate::Route)s.
pub children: Box<dyn Fn() -> Vec<Element>>,
}
/// Provides for client-side and server-side routing. This should usually be somewhere near
/// the root of the application.
#[allow(non_snake_case)]
pub fn Router(cx: Scope, props: RouterProps) -> impl IntoChild {
// create a new RouterContext and provide it to every component beneath the router
@@ -38,6 +46,7 @@ pub fn Router(cx: Scope, props: RouterProps) -> impl IntoChild {
props.children
}
/// Context type that contains information about the current router state.
#[derive(Debug, Clone)]
pub struct RouterContext {
pub(crate) inner: Rc<RouterContextInner>,
@@ -72,7 +81,11 @@ impl std::fmt::Debug for RouterContextInner {
}
impl RouterContext {
pub fn new(cx: Scope, base: Option<&'static str>, fallback: Option<fn() -> Element>) -> Self {
pub(crate) fn new(
cx: Scope,
base: Option<&'static str>,
fallback: Option<fn() -> Element>,
) -> Self {
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
let history = use_context::<RouterIntegrationContext>(cx)
@@ -157,10 +170,12 @@ impl RouterContext {
Self { inner }
}
/// The current [`pathname`](https://developer.mozilla.org/en-US/docs/Web/API/Location/pathname).
pub fn pathname(&self) -> Memo<String> {
self.inner.location.pathname
}
/// The [RouteContext] of the base route.
pub fn base(&self) -> RouteContext {
self.inner.base.clone()
}
@@ -190,7 +205,7 @@ impl RouterContextInner {
return Err(NavigationError::MaxRedirects);
}
if resolved_to != (this.reference)() || options.state != (this.state).get() {
if resolved_to != this.reference.get() || options.state != (this.state).get() {
if cfg!(feature = "server") {
// TODO server out
self.history.navigate(&LocationChange {
@@ -336,18 +351,30 @@ impl RouterContextInner {
}
}
/// An error that occurs during navigation.
#[derive(Debug, Error)]
pub enum NavigationError {
/// The given path is not routable.
#[error("Path {0:?} is not routable")]
NotRoutable(String),
/// Too many redirects occurred during routing (prevents and infinite loop.)
#[error("Too many redirects")]
MaxRedirects,
}
/// Options that can be used to configure a navigation. Used with [use_navigate](crate::use_navigate).
#[derive(Clone, Debug)]
pub struct NavigateOptions {
/// Whether the URL being navigated to should be resolved relative to the current route.
pub resolve: bool,
/// If `true` the new location will replace the current route in the history stack, meaning
/// the "back" button will skip over the current route. (Defaults to `false`).
pub replace: bool,
/// If `true`, the router will scroll to the top of the window at the end of navigation.
/// Defaults to `true.
pub scroll: bool,
/// [State](https://developer.mozilla.org/en-US/docs/Web/API/History/state) that should be pushed
/// onto the history stack during navigation.
pub state: State,
}

View File

@@ -5,6 +5,7 @@ use typed_builder::TypedBuilder;
use crate::{matching::{expand_optionals, join_paths, Branch, Matcher, RouteDefinition, get_route_matches, RouteMatch}, RouterContext, RouteContext};
/// Props for the [Routes] component, which contains route definitions and manages routing.
#[derive(TypedBuilder)]
pub struct RoutesProps {
#[builder(default, setter(strip_option))]
@@ -12,6 +13,9 @@ pub struct RoutesProps {
children: Box<dyn Fn() -> Vec<RouteDefinition>>,
}
/// 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 {
let router = use_context::<RouterContext>(cx).unwrap_or_else(|| {

View File

@@ -1,32 +0,0 @@
use std::{future::Future, rc::Rc};
use crate::{PinnedFuture, Request, Response};
#[derive(Clone)]
pub struct Action {
f: Rc<dyn Fn(&Request) -> PinnedFuture<Response>>,
}
impl Action {
pub async fn send(&self, req: &Request) -> Response {
(self.f)(req).await
}
}
impl<F, Fu> From<F> for Action
where
F: Fn(&Request) -> Fu + 'static,
Fu: Future<Output = Response> + 'static,
{
fn from(f: F) -> Self {
Self {
f: Rc::new(move |req| Box::pin(f(req))),
}
}
}
impl std::fmt::Debug for Action {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Action").finish()
}
}

View File

@@ -1,207 +0,0 @@
use std::{any::Any, fmt::Debug, future::Future, rc::Rc};
use leptos::*;
use crate::{use_location, use_params_map, use_route, ParamsMap, PinnedFuture, Url};
// SSR and CSR both do the work in their own environment and return it as a resource
#[cfg(not(feature = "hydrate"))]
pub fn use_loader<T>(cx: Scope) -> Resource<(ParamsMap, Url), T>
where
T: Clone + Debug + Serializable + 'static,
{
let route = use_route(cx);
let params = use_params_map(cx);
let loader = route.loader().clone().unwrap_or_else(|| {
debug_warn!(
"use_loader() called on a route without a loader: {:?}",
route.path()
);
panic!()
});
let location = use_location(cx);
let route = use_route(cx);
let url = move || Url {
origin: String::default(), // don't care what the origin is for this purpose
pathname: route.path().into(), // only use this route path, not all matched routes
search: location.search.get(), // reload when any of query string changes
hash: String::default(), // hash is only client-side, shouldn't refire
};
let loader = loader.data.clone();
create_resource(
cx,
move || (params.get(), url()),
move |(params, url)| {
let loader = loader.clone();
async move {
let any_data = (loader.clone())(cx, params, url).await;
any_data
.as_any()
.downcast_ref::<T>()
.cloned()
.unwrap_or_else(|| {
panic!(
"use_loader() could not downcast to {:?}",
std::any::type_name::<T>(),
)
})
}
},
)
}
// In hydration mode, only run the loader on the server
#[cfg(feature = "hydrate")]
pub fn use_loader<T>(cx: Scope) -> Resource<(ParamsMap, Url), T>
where
T: Clone + Debug + Serializable + 'static,
{
use wasm_bindgen::{JsCast, UnwrapThrowExt};
use crate::use_query_map;
let route = use_route(cx);
let params = use_params_map(cx);
let location = use_location(cx);
let route = use_route(cx);
let url = move || Url {
origin: String::default(), // don't care what the origin is for this purpose
pathname: route.path().into(), // only use this route path, not all matched routes
search: location.search.get(), // reload when any of query string changes
hash: String::default(), // hash is only client-side, shouldn't refire
};
create_resource(
cx,
move || (params.get(), url()),
move |(params, url)| async move {
let route = use_route(cx);
let query = use_query_map(cx);
let mut opts = web_sys::RequestInit::new();
opts.method("GET");
let url = format!("{}{}", route.path(), query.get().to_query_string());
let request = web_sys::Request::new_with_str_and_init(&url, &opts).unwrap_throw();
request
.headers()
.set("Accept", "application/json")
.unwrap_throw();
let window = web_sys::window().unwrap_throw();
let resp_value =
wasm_bindgen_futures::JsFuture::from(window.fetch_with_request(&request))
.await
.unwrap_throw();
let resp = resp_value.unchecked_into::<web_sys::Response>();
let text = wasm_bindgen_futures::JsFuture::from(resp.text().unwrap_throw())
.await
.unwrap_throw()
.as_string()
.unwrap_throw();
T::from_json(&text).expect_throw(
"couldn't deserialize loader data from serde-lite intermediate format",
)
},
)
}
pub trait AnySerialize {
fn serialize(&self) -> Option<String>;
fn as_any(&self) -> &dyn Any;
}
impl<T> AnySerialize for T
where
T: Any + Serializable + 'static,
{
fn serialize(&self) -> Option<String> {
self.to_json().ok()
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[derive(Clone)]
pub struct Loader {
#[allow(clippy::type_complexity)]
#[cfg(not(feature = "hydrate"))]
pub(crate) data: Rc<dyn Fn(Scope, ParamsMap, Url) -> PinnedFuture<Box<dyn AnySerialize>>>,
}
impl Loader {
#[cfg(not(feature = "hydrate"))]
pub fn call_loader(&self, cx: Scope) -> PinnedFuture<Box<dyn AnySerialize>> {
let route = use_route(cx);
let params = use_params_map(cx).get();
let location = use_location(cx);
let url = Url {
origin: String::default(), // don't care what the origin is for this purpose
pathname: route.path().into(), // only use this route path, not all matched routes
search: location.search.get(), // reload when any of query string changes
hash: String::default(), // hash is only client-side, shouldn't refire
};
(self.data)(cx, params, url)
}
}
impl<F, Fu, T> From<F> for Loader
where
F: Fn(Scope, ParamsMap, Url) -> Fu + 'static,
Fu: Future<Output = T> + 'static,
T: Any + Serializable + 'static,
{
#[cfg(not(feature = "hydrate"))]
fn from(f: F) -> Self {
let wrapped_fn = move |cx, params, url| {
let res = f(cx, params, url);
Box::pin(async move { Box::new(res.await) as Box<dyn AnySerialize> })
as PinnedFuture<Box<dyn AnySerialize>>
};
Self {
data: Rc::new(wrapped_fn),
}
}
#[cfg(feature = "hydrate")]
fn from(f: F) -> Self {
Self {}
}
}
impl std::fmt::Debug for Loader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Loader").finish()
}
}
#[cfg(all(feature = "ssr", not(feature = "hydrate")))]
pub async fn loader_to_json(view: impl Fn(Scope) -> String + 'static) -> Option<String> {
let (data, _, disposer) = run_scope_undisposed(move |cx| async move {
let _shell = view(cx);
let mut route = use_context::<crate::RouteContext>(cx)?;
// get the innermost route matched by this path
while let Some(child) = route.child() {
route = child;
}
let data = route
.loader()
.as_ref()
.map(|loader| loader.call_loader(cx))?;
data.await.serialize()
});
let data = data.await;
disposer.dispose();
data
}

View File

@@ -1,9 +0,0 @@
mod action;
mod loader;
use std::{future::Future, pin::Pin};
pub use action::*;
pub use loader::*;
pub(crate) type PinnedFuture<T> = Pin<Box<dyn Future<Output = T>>>;

View File

@@ -1,27 +0,0 @@
use std::rc::Rc;
use thiserror::Error;
#[derive(Error, Debug, Clone)]
pub enum RouterError {
#[error("loader found no data at this path")]
NoMatch(String),
#[error("route was matched, but loader returned None")]
NotFound(String),
#[error("could not find parameter {0}")]
MissingParam(String),
#[error("failed to deserialize parameters")]
Params(Rc<dyn std::error::Error + Send + Sync>),
}
impl PartialEq for RouterError {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::NoMatch(l0), Self::NoMatch(r0)) => l0 == r0,
(Self::NotFound(l0), Self::NotFound(r0)) => l0 == r0,
(Self::MissingParam(l0), Self::MissingParam(r0)) => l0 == r0,
(Self::Params(_), Self::Params(_)) => false,
_ => false,
}
}
}

View File

@@ -4,6 +4,7 @@ use crate::{State, Url};
use super::params::ParamsMap;
/// Creates a reactive location from the given path and state.
pub fn create_location(cx: Scope, path: ReadSignal<String>, state: ReadSignal<State>) -> Location {
let url = create_memo(cx, move |prev: Option<&Url>| {
path.with(|path| match Url::try_from(path.as_str()) {
@@ -29,20 +30,33 @@ pub fn create_location(cx: Scope, path: ReadSignal<String>, state: ReadSignal<St
}
}
/// A reactive description of the current URL, containing equivalents to the local parts of
/// the browser's [`Location`](https://developer.mozilla.org/en-US/docs/Web/API/Location).
#[derive(Debug, Clone, PartialEq)]
pub struct Location {
pub query: Memo<ParamsMap>,
/// The path of the URL, not containing the query string or hash fragment.
pub pathname: Memo<String>,
/// The raw query string.
pub search: Memo<String>,
/// The query string parsed into its key-value pairs.
pub query: Memo<ParamsMap>,
/// The hash fragment.
pub hash: Memo<String>,
/// The [`state`](https://developer.mozilla.org/en-US/docs/Web/API/History/state) at the top of the history stack.
pub state: ReadSignal<State>,
}
/// A description of a navigation.
#[derive(Debug, Clone, PartialEq)]
pub struct LocationChange {
/// The new URL.
pub value: String,
/// If true, the new location will replace the current one in the history stack, i.e.,
/// clicking the "back" button will not return to the current location.
pub replace: bool,
/// If true, the router will scroll to the top of the page at the end of the navigation.
pub scroll: bool,
/// The [`state`](https://developer.mozilla.org/en-US/docs/Web/API/History/state) that will be added during navigation.
pub state: State,
}

View File

@@ -18,12 +18,19 @@ impl std::fmt::Debug for RouterIntegrationContext {
}
}
/// The [Router](crate::Router) relies on a [RouterIntegrationContext], which tells the router
/// how to find things like the current URL, and how to navigate to a new page. The [History] trait
/// can be implemented on any type to provide this information.
pub trait History {
/// A signal that updates whenever the current location changes.
fn location(&self, cx: Scope) -> ReadSignal<LocationChange>;
/// Called to navigate to a new location.
fn navigate(&self, loc: &LocationChange);
}
/// The default integration when you are running in the browser, which uses
/// the [`History API`](https://developer.mozilla.org/en-US/docs/Web/API/History).
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct BrowserIntegration {}
@@ -66,7 +73,7 @@ impl History for BrowserIntegration {
) {
log::error!("{e:#?}");
}
set_location(Self::current());
set_location.set(Self::current());
} else {
log::warn!("RouterContext not found");
}
@@ -105,10 +112,23 @@ impl History for BrowserIntegration {
}
}
/// The wrapper type that the [Router](crate::Router) uses to interact with a [History].
/// This is automatically provided in the browser. For the server, it should be provided
/// as a context.
///
/// ```
/// # use leptos_router::*;
/// # use leptos::*;
/// # run_scope(|cx| {
/// let integration = ServerIntegration { path: "insert/current/path/here".to_string() };
/// provide_context(cx, RouterIntegrationContext::new(integration));
/// # });
/// ```
#[derive(Clone)]
pub struct RouterIntegrationContext(pub Rc<dyn History>);
impl RouterIntegrationContext {
/// Creates a new router integration.
pub fn new(history: impl History + 'static) -> Self {
Self(Rc::new(history))
}
@@ -124,6 +144,7 @@ impl History for RouterIntegrationContext {
}
}
/// A generic router integration for the server side. All its need is the current path.
#[derive(Clone, Debug)]
pub struct ServerIntegration {
pub path: String,

View File

@@ -1,9 +1,8 @@
use std::{rc::Rc, str::FromStr};
use linear_map::LinearMap;
use std::{rc::Rc, str::FromStr};
use thiserror::Error;
use crate::RouterError;
/// A key-value map of the current named route params and their values.
// For now, implemented with a `LinearMap`, as `n` is small enough
// that O(n) iteration over a vectorized map is (*probably*) more space-
// and time-efficient than hashing and using an actual `HashMap`
@@ -11,27 +10,33 @@ use crate::RouterError;
pub struct ParamsMap(pub LinearMap<String, String>);
impl ParamsMap {
/// Creates an empty map.
pub fn new() -> Self {
Self(LinearMap::new())
}
/// Creates an empty map with the given capacity.
pub fn with_capacity(capacity: usize) -> Self {
Self(LinearMap::with_capacity(capacity))
}
/// Inserts a value into the map.
pub fn insert(&mut self, key: String, value: String) -> Option<String> {
self.0.insert(key, value)
}
/// Gets a value from the map.
pub fn get(&self, key: &str) -> Option<&String> {
self.0.get(key)
}
/// Removes a value from the map.
pub fn remove(&mut self, key: &str) -> Option<String> {
self.0.remove(key)
}
#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))]
/// Converts the map to a query string.
pub fn to_query_string(&self) -> String {
use crate::history::url::escape;
let mut buf = String::from("?");
@@ -51,6 +56,16 @@ impl Default for ParamsMap {
}
}
/// A declarative way of creating a [ParamsMap].
///
/// ```
/// # use leptos_router::params_map;
/// let map = params_map! {
/// "id".to_string() => "1".to_string()
/// };
/// assert_eq!(map.get("id"), Some(&"1".to_string()));
/// assert_eq!(map.get("missing"), None)
/// ```
// Adapted from hash_map! in common_macros crate
// Copyright (c) 2019 Philipp Korber
// https://github.com/rustonaut/common_macros/blob/master/src/lib.rs
@@ -68,15 +83,19 @@ macro_rules! params_map {
});
}
/// A simple method of deserializing key-value data (like route params or URL search)
/// into a concrete data type. `Self` should typically be a struct in which
/// each field's type implements [FromStr].
pub trait Params
where
Self: Sized,
{
fn from_map(map: &ParamsMap) -> Result<Self, RouterError>;
/// Attempts to deserialize the map into the given type.
fn from_map(map: &ParamsMap) -> Result<Self, ParamsError>;
}
impl Params for () {
fn from_map(_map: &ParamsMap) -> Result<Self, RouterError> {
fn from_map(_map: &ParamsMap) -> Result<Self, ParamsError> {
Ok(())
}
}
@@ -85,22 +104,22 @@ pub trait IntoParam
where
Self: Sized,
{
fn into_param(value: Option<&str>, name: &str) -> Result<Self, RouterError>;
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 + Send + Sync + 'static,
<T as FromStr>::Err: std::error::Error + 'static,
{
fn into_param(value: Option<&str>, _name: &str) -> Result<Self, RouterError> {
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(RouterError::Params(Rc::new(e)))
Err(ParamsError::Params(Rc::new(e)))
}
},
}
@@ -115,8 +134,29 @@ where
T: FromStr + NotOption,
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
{
fn into_param(value: Option<&str>, name: &str) -> Result<Self, RouterError> {
let value = value.ok_or_else(|| RouterError::MissingParam(name.to_string()))?;
Self::from_str(value).map_err(|e| RouterError::Params(Rc::new(e)))
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)))
}
}
/// Errors that can occur while parsing params using [IntoParams].
#[derive(Error, Debug, Clone)]
pub enum ParamsError {
/// A field was missing from the route params.
#[error("could not find parameter {0}")]
MissingParam(String),
/// Something went wrong while deserializing a field.
#[error("failed to deserialize parameters")]
Params(Rc<dyn std::error::Error>),
}
impl PartialEq for ParamsError {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::MissingParam(l0), Self::MissingParam(r0)) => l0 == r0,
(Self::Params(_), Self::Params(_)) => false,
_ => false,
}
}
}

View File

@@ -3,10 +3,11 @@ use std::rc::Rc;
use leptos::{create_memo, use_context, Memo, Scope};
use crate::{
Location, NavigateOptions, NavigationError, Params, ParamsMap, RouteContext, RouterContext,
RouterError,
Location, NavigateOptions, NavigationError, Params, ParamsError, ParamsMap, RouteContext,
RouterContext,
};
/// Returns the current [RouterContext], containing information about the router's state.
pub fn use_router(cx: Scope) -> RouterContext {
if let Some(router) = use_context::<RouterContext>(cx) {
router
@@ -16,15 +17,24 @@ pub fn use_router(cx: Scope) -> RouterContext {
}
}
/// Returns the current [RouteContext], containing information about the matched route.
pub fn use_route(cx: Scope) -> RouteContext {
use_context::<RouteContext>(cx).unwrap_or_else(|| use_router(cx).base())
}
/// Returns the current [Location], which contains reactive variables
pub fn use_location(cx: Scope) -> Location {
use_router(cx).inner.location.clone()
}
pub fn use_params<T: Params>(cx: Scope) -> Memo<Result<T, RouterError>>
/// Returns a raw key-value map of route params.
pub fn use_params_map(cx: Scope) -> Memo<ParamsMap> {
let route = use_route(cx);
route.params()
}
/// Returns the current route params, parsed into the given type, or an error.
pub fn use_params<T: Params>(cx: Scope) -> Memo<Result<T, ParamsError>>
where
T: PartialEq + std::fmt::Debug,
{
@@ -32,12 +42,13 @@ where
create_memo(cx, move |_| route.params().with(T::from_map))
}
pub fn use_params_map(cx: Scope) -> Memo<ParamsMap> {
let route = use_route(cx);
route.params()
/// Returns a raw key-value map of the URL search query.
pub fn use_query_map(cx: Scope) -> Memo<ParamsMap> {
use_router(cx).inner.location.query
}
pub fn use_query<T: Params>(cx: Scope) -> Memo<Result<T, RouterError>>
/// Returns the current URL search query, parsed into the given type, or an error.
pub fn use_query<T: Params>(cx: Scope) -> Memo<Result<T, ParamsError>>
where
T: PartialEq + std::fmt::Debug,
{
@@ -47,16 +58,14 @@ where
})
}
pub fn use_query_map(cx: Scope) -> Memo<ParamsMap> {
use_router(cx).inner.location.query
}
/// Resolves the given path relative to the current route.
pub fn use_resolved_path(cx: Scope, path: impl Fn() -> String + 'static) -> Memo<Option<String>> {
let route = use_route(cx);
create_memo(cx, move |_| route.resolve_path(&path()).map(String::from))
}
/// Returns a function that can be used to navigate to a new route.
pub fn use_navigate(cx: Scope) -> impl Fn(&str, NavigateOptions) -> Result<(), NavigationError> {
let router = use_router(cx);
move |to, options| Rc::clone(&router.inner).navigate_from_route(to, &options)

View File

@@ -6,10 +6,8 @@
//! apps (SPAs), server-side rendering/multi-page apps (MPAs), or to synchronize
//! state between the two.
//!
//! **Note:** This is a work in progress. Docs are still being written,
//! and some features are only stubs, in particular
//! - passing client-side route [State] in [History.state](https://developer.mozilla.org/en-US/docs/Web/API/History/state))
//! - data mutations using [Action]s and [Form] `method="POST"`
//! **Note:** This is a work in progress. Docs are still being written, in particular
//! passing client-side route [State] in [History.state](https://developer.mozilla.org/en-US/docs/Web/API/History/state))
//!
//! ## Philosophy
//!
@@ -62,20 +60,13 @@
//! <Route
//! path=""
//! element=move |cx| view! { cx, <ContactList/> }
//! // <ContactList/> needs all the contacts, so we provide the loader here
//! // this will only be reloaded if we navigate away to /about and back to / or /:id
//! loader=contact_list_data.into()
//! >
//! // users like /gbj or /bob
//! <Route
//! path=":id"
//! // <Contact/> needs contact data, so we provide the loader here
//! // this will be reloaded when the :id changes
//! loader=contact_data.into()
//! element=move |cx| view! { cx, <Contact/> }
//! />
//! // a fallback if the /:id segment is missing from the URL
//! // doesn't need any data, so no loader is provided
//! <Route
//! path=""
//! element=move |_| view! { cx, <p class="contact">"Select a contact."</p> }
@@ -93,33 +84,46 @@
//! }
//! }
//!
//! // Loaders are async functions that have access to the reactive scope,
//! // map of matched URL params for that route, and the URL
//! // They are reloaded whenever the params or URL change
//!
//! type ContactSummary = (); // TODO!
//! type Contact = (); // TODO!()
//!
//! // contact_data reruns whenever the :id param changes
//! async fn contact_data(_cx: Scope, _params: ParamsMap, url: Url) -> Contact {
//! async fn contact_data(id: String) -> Contact {
//! todo!()
//! }
//!
//! // contact_list_data *doesn't* rerun when the :id changes,
//! // because that param is nested lower than the <ContactList/> route
//! async fn contact_list_data(_cx: Scope, _params: ParamsMap, url: Url) -> Vec<ContactSummary> {
//! async fn contact_list_data() -> Vec<ContactSummary> {
//! todo!()
//! }
//!
//! #[component]
//! fn ContactList(cx: Scope) -> Element {
//! let data = use_loader::<Vec<ContactSummary>>(cx);
//! todo!()
//! // loads the contact list data once; doesn't reload when nested routes change
//! let contacts = create_resource(cx, || (), |_| contact_list_data());
//! view! {
//! cx,
//! <div>
//! // show the contacts
//! <ul>
//! {move || contacts.read().map(|contacts| view! { cx, <li>"todo contact info"</li> } )}
//! </ul>
//!
//! // insert the nested child route here
//! <Outlet/>
//! </div>
//! }
//! }
//!
//! #[component]
//! fn Contact(cx: Scope) -> Element {
//! let data = use_loader::<Contact>(cx);
//! let params = use_params_map(cx);
//! let data = create_resource(
//! cx,
//! move || params.with(|p| p.get("id").cloned().unwrap_or_default()),
//! move |id| contact_data(id)
//! );
//! todo!()
//! }
//!
@@ -136,16 +140,12 @@
#![feature(type_name_of_val)]
mod components;
mod data;
mod error;
mod fetch;
mod history;
mod hooks;
mod matching;
pub use components::*;
pub use data::*;
pub use error::*;
pub use fetch::*;
pub use history::*;
pub use hooks::*;

View File

@@ -25,9 +25,12 @@ pub(crate) fn get_route_matches(branches: Vec<Branch>, location: String) -> Vec<
vec![]
}
/// Describes a branch of the route tree.
#[derive(Debug, Clone, PartialEq)]
pub struct Branch {
/// All the routes contained in the branch.
pub routes: Vec<RouteData>,
/// How closely this branch matches the current URL.
pub score: i32,
}

View File

@@ -3,13 +3,9 @@ use std::rc::Rc;
use leptos::leptos_dom::Child;
use leptos::*;
use crate::{Action, Loader};
#[derive(Clone)]
pub struct RouteDefinition {
pub path: &'static str,
pub loader: Option<Loader>,
pub action: Option<Action>,
pub children: Vec<RouteDefinition>,
pub element: Rc<dyn Fn(Scope) -> Child>,
}
@@ -18,8 +14,6 @@ impl std::fmt::Debug for RouteDefinition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RouteDefinition")
.field("path", &self.path)
.field("loader", &self.loader)
.field("action", &self.action)
.field("children", &self.children)
.finish()
}
@@ -35,8 +29,6 @@ impl Default for RouteDefinition {
fn default() -> Self {
Self {
path: Default::default(),
loader: Default::default(),
action: Default::default(),
children: Default::default(),
element: Rc::new(|_| Child::Null),
}