mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 10:11:56 -05:00
Compare commits
12 Commits
allow-on-d
...
remove-loa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2483616d0d | ||
|
|
2595ffe10e | ||
|
|
221cdf2685 | ||
|
|
bd652ec542 | ||
|
|
d8852f909e | ||
|
|
e16cc4fc4a | ||
|
|
d5e3661bcf | ||
|
|
8873ddc40a | ||
|
|
b7e2e983f0 | ||
|
|
3701f65693 | ||
|
|
a5712d3e17 | ||
|
|
4fba035f19 |
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos"
|
||||
version = "0.0.11"
|
||||
version = "0.0.12"
|
||||
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.12" }
|
||||
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.12" }
|
||||
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.12" }
|
||||
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.12" }
|
||||
leptos_server = { path = "../leptos_server", default-features = false, version = "0.0.12" }
|
||||
|
||||
[features]
|
||||
default = ["csr", "serde"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_core"
|
||||
version = "0.0.11"
|
||||
version = "0.0.12"
|
||||
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.12" }
|
||||
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.12" }
|
||||
log = "0.4"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_macro"
|
||||
version = "0.0.11"
|
||||
version = "0.0.12"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -17,8 +17,8 @@ quote = "1"
|
||||
syn = { version = "1", features = ["full", "parsing", "extra-traits"] }
|
||||
syn-rsx = "0.8.1"
|
||||
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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_reactive"
|
||||
version = "0.0.11"
|
||||
version = "0.0.12"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
[package]
|
||||
name = "leptos_server"
|
||||
version = "0.0.11"
|
||||
version = "0.0.12"
|
||||
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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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(|| {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>>>;
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user