mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
remove old router files
This commit is contained in:
parent
a29ffc8dcb
commit
3382047857
44 changed files with 0 additions and 7958 deletions
|
@ -1,80 +0,0 @@
|
|||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.7.0-preview2"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston", "Ben Wishovich"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Router for the Leptos web framework."
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { workspace = true }
|
||||
leptos_integration_utils = { workspace = true, optional = true }
|
||||
leptos_meta = { workspace = true, optional = true }
|
||||
cached = { version = "0.45.0", optional = true }
|
||||
cfg-if = "1"
|
||||
gloo-net = { version = "0.5", features = ["http"] }
|
||||
lazy_static = "1"
|
||||
linear-map = { version = "1", features = ["serde_impl"] }
|
||||
once_cell = "1.18"
|
||||
regex = { version = "1", optional = true }
|
||||
url = { version = "2", optional = true }
|
||||
percent-encoding = "2"
|
||||
thiserror = "1"
|
||||
serde_qs = "0.12"
|
||||
serde = "1"
|
||||
tracing = "0.1"
|
||||
js-sys = { version = "0.3" }
|
||||
wasm-bindgen = { version = "0.2" }
|
||||
wasm-bindgen-futures = { version = "0.4" }
|
||||
lru = { version = "0.11", optional = true }
|
||||
serde_json = "1.0.96"
|
||||
itertools = "0.12.0"
|
||||
send_wrapper = "0.6.0"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
# History/Routing
|
||||
"History",
|
||||
"HtmlAnchorElement",
|
||||
"MouseEvent",
|
||||
"Url",
|
||||
# Form
|
||||
"FormData",
|
||||
"HtmlButtonElement",
|
||||
"HtmlFormElement",
|
||||
"HtmlInputElement",
|
||||
"SubmitEvent",
|
||||
"Url",
|
||||
"UrlSearchParams",
|
||||
# Fetching in Hydrate Mode
|
||||
"Headers",
|
||||
"Request",
|
||||
"RequestInit",
|
||||
"RequestMode",
|
||||
"Response",
|
||||
"Window",
|
||||
]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
csr = ["leptos/csr"]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = [
|
||||
"leptos/ssr",
|
||||
"dep:cached",
|
||||
"dep:lru",
|
||||
"dep:url",
|
||||
"dep:regex",
|
||||
"dep:leptos_integration_utils",
|
||||
"dep:leptos_meta",
|
||||
]
|
||||
nightly = ["leptos/nightly"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
# No need to test optional dependencies as they are enabled by the ssr feature
|
||||
denylist = ["url", "regex", "nightly"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
|
@ -1 +0,0 @@
|
|||
extend = { path = "../cargo-make/main.toml" }
|
|
@ -1,96 +0,0 @@
|
|||
/// Configures what animation should be shown when transitioning
|
||||
/// between two root routes. Defaults to `None`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct Animation {
|
||||
/// Class set when a route is first painted.
|
||||
pub start: Option<&'static str>,
|
||||
/// Class set when a route is fading out.
|
||||
pub outro: Option<&'static str>,
|
||||
/// Class set when a route is fading in.
|
||||
pub intro: Option<&'static str>,
|
||||
/// Class set when a route is fading out, if it’s a “back” navigation.
|
||||
pub outro_back: Option<&'static str>,
|
||||
/// Class set when a route is fading in, if it’s a “back” navigation.
|
||||
pub intro_back: Option<&'static str>,
|
||||
/// Class set when all animations have finished.
|
||||
pub finally: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl Animation {
|
||||
pub(crate) fn next_state(
|
||||
&self,
|
||||
current: &AnimationState,
|
||||
is_back: bool,
|
||||
) -> (AnimationState, bool) {
|
||||
let Animation {
|
||||
start,
|
||||
outro,
|
||||
intro,
|
||||
intro_back,
|
||||
..
|
||||
} = self;
|
||||
match current {
|
||||
AnimationState::Outro => {
|
||||
let next = if start.is_some() {
|
||||
AnimationState::Start
|
||||
} else if intro.is_some() {
|
||||
AnimationState::Intro
|
||||
} else {
|
||||
AnimationState::Finally
|
||||
};
|
||||
(next, true)
|
||||
}
|
||||
AnimationState::OutroBack => {
|
||||
let next = if start.is_some() {
|
||||
AnimationState::Start
|
||||
} else if intro_back.is_some() {
|
||||
AnimationState::IntroBack
|
||||
} else if intro.is_some() {
|
||||
AnimationState::Intro
|
||||
} else {
|
||||
AnimationState::Finally
|
||||
};
|
||||
(next, true)
|
||||
}
|
||||
AnimationState::Start => {
|
||||
let next = if intro.is_some() {
|
||||
AnimationState::Intro
|
||||
} else {
|
||||
AnimationState::Finally
|
||||
};
|
||||
(next, false)
|
||||
}
|
||||
AnimationState::Intro => (AnimationState::Finally, false),
|
||||
AnimationState::IntroBack => (AnimationState::Finally, false),
|
||||
AnimationState::Finally => {
|
||||
if outro.is_some() {
|
||||
if is_back {
|
||||
(AnimationState::OutroBack, false)
|
||||
} else {
|
||||
(AnimationState::Outro, false)
|
||||
}
|
||||
} else if start.is_some() {
|
||||
(AnimationState::Start, true)
|
||||
} else if intro.is_some() {
|
||||
if is_back {
|
||||
(AnimationState::IntroBack, false)
|
||||
} else {
|
||||
(AnimationState::Intro, false)
|
||||
}
|
||||
} else {
|
||||
(AnimationState::Finally, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, PartialOrd, Ord)]
|
||||
pub(crate) enum AnimationState {
|
||||
Outro,
|
||||
OutroBack,
|
||||
Start,
|
||||
Intro,
|
||||
IntroBack,
|
||||
Finally,
|
||||
}
|
|
@ -1,838 +0,0 @@
|
|||
use crate::{
|
||||
hooks::has_router, resolve_redirect_url, use_navigate, use_resolved_path,
|
||||
NavigateOptions, ToHref, Url,
|
||||
};
|
||||
use leptos::{
|
||||
html::form,
|
||||
logging::*,
|
||||
server_fn::{client::Client, codec::PostUrl, request::ClientReq, ServerFn},
|
||||
*,
|
||||
};
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::{error::Error, fmt::Debug, rc::Rc};
|
||||
use thiserror::Error;
|
||||
use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt};
|
||||
use web_sys::{
|
||||
Event, FormData, HtmlButtonElement, HtmlFormElement, HtmlInputElement,
|
||||
RequestRedirect, SubmitEvent,
|
||||
};
|
||||
|
||||
type OnFormData = Rc<dyn Fn(&web_sys::FormData)>;
|
||||
type OnResponse = Rc<dyn Fn(&web_sys::Response)>;
|
||||
type OnError = Rc<dyn Fn(&gloo_net::Error)>;
|
||||
|
||||
/// An HTML [`form`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form) progressively
|
||||
/// enhanced to use client-side routing.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[component]
|
||||
pub fn Form<A>(
|
||||
/// [`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`).
|
||||
#[prop(optional)]
|
||||
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`].
|
||||
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`.
|
||||
#[prop(optional)]
|
||||
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.
|
||||
#[prop(optional)]
|
||||
version: Option<RwSignal<usize>>,
|
||||
/// A signal that will be set if the form submission ends in an error.
|
||||
#[prop(optional)]
|
||||
error: Option<RwSignal<Option<Box<dyn Error>>>>,
|
||||
/// A callback will be called with the [`FormData`](web_sys::FormData) when the form is submitted.
|
||||
#[prop(optional)]
|
||||
on_form_data: Option<OnFormData>,
|
||||
/// Sets the `class` attribute on the underlying `<form>` tag, making it easier to style.
|
||||
#[prop(optional, into)]
|
||||
class: Option<AttributeValue>,
|
||||
/// A callback will be called with the [`Response`](web_sys::Response) the server sends in response
|
||||
/// to a form submission.
|
||||
#[prop(optional)]
|
||||
on_response: Option<OnResponse>,
|
||||
/// A callback will be called if the attempt to submit the form results in an error.
|
||||
#[prop(optional)]
|
||||
on_error: Option<OnError>,
|
||||
/// A [`NodeRef`] in which the `<form>` element should be stored.
|
||||
#[prop(optional)]
|
||||
node_ref: Option<NodeRef<html::Form>>,
|
||||
/// Sets whether the page should be scrolled to the top when the form is submitted.
|
||||
#[prop(optional)]
|
||||
noscroll: bool,
|
||||
/// Sets whether the page should replace the current location in the history when the form is submitted.
|
||||
#[prop(optional)]
|
||||
replace: bool,
|
||||
/// Arbitrary attributes to add to the `<form>`. Attributes can be added with the
|
||||
/// `attr:` syntax in the `view` macro.
|
||||
#[prop(attrs)]
|
||||
attributes: Vec<(&'static str, Attribute)>,
|
||||
/// Component children; should include the HTML of the form elements.
|
||||
children: Children,
|
||||
) -> impl IntoView
|
||||
where
|
||||
A: ToHref + 'static,
|
||||
{
|
||||
async fn post_form_data(
|
||||
action: &str,
|
||||
form_data: FormData,
|
||||
) -> Result<gloo_net::http::Response, gloo_net::Error> {
|
||||
gloo_net::http::Request::post(action)
|
||||
.header("Accept", "application/json")
|
||||
.redirect(RequestRedirect::Follow)
|
||||
.body(form_data)?
|
||||
.send()
|
||||
.await
|
||||
}
|
||||
|
||||
async fn post_params(
|
||||
action: &str,
|
||||
enctype: &str,
|
||||
params: web_sys::UrlSearchParams,
|
||||
) -> Result<gloo_net::http::Response, gloo_net::Error> {
|
||||
gloo_net::http::Request::post(action)
|
||||
.header("Accept", "application/json")
|
||||
.header("Content-Type", enctype)
|
||||
.redirect(RequestRedirect::Follow)
|
||||
.body(params)?
|
||||
.send()
|
||||
.await
|
||||
}
|
||||
|
||||
fn inner(
|
||||
has_router: bool,
|
||||
method: Option<&'static str>,
|
||||
action: Memo<Option<String>>,
|
||||
enctype: Option<String>,
|
||||
version: Option<RwSignal<usize>>,
|
||||
error: Option<RwSignal<Option<Box<dyn Error>>>>,
|
||||
on_form_data: Option<OnFormData>,
|
||||
on_response: Option<OnResponse>,
|
||||
on_error: Option<OnError>,
|
||||
class: Option<Attribute>,
|
||||
children: Children,
|
||||
node_ref: Option<NodeRef<html::Form>>,
|
||||
noscroll: bool,
|
||||
replace: bool,
|
||||
attributes: Vec<(&'static str, Attribute)>,
|
||||
) -> HtmlElement<html::Form> {
|
||||
let action_version = version;
|
||||
let on_submit = {
|
||||
move |ev: web_sys::SubmitEvent| {
|
||||
if ev.default_prevented() {
|
||||
return;
|
||||
}
|
||||
let navigate = has_router.then(use_navigate);
|
||||
let navigate_options = NavigateOptions {
|
||||
scroll: !noscroll,
|
||||
replace,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (form, method, action, enctype) =
|
||||
extract_form_attributes(&ev);
|
||||
|
||||
let form_data =
|
||||
web_sys::FormData::new_with_form(&form).unwrap_throw();
|
||||
if let Some(on_form_data) = on_form_data.clone() {
|
||||
on_form_data(&form_data);
|
||||
}
|
||||
let params =
|
||||
web_sys::UrlSearchParams::new_with_str_sequence_sequence(
|
||||
&form_data,
|
||||
)
|
||||
.unwrap_throw();
|
||||
let action = if has_router {
|
||||
use_resolved_path(move || action.clone())
|
||||
.get_untracked()
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
action
|
||||
};
|
||||
// multipart POST (setting Context-Type breaks the request)
|
||||
if method == "post" && enctype == "multipart/form-data" {
|
||||
ev.prevent_default();
|
||||
ev.stop_propagation();
|
||||
|
||||
let on_response = on_response.clone();
|
||||
let on_error = on_error.clone();
|
||||
spawn_local(async move {
|
||||
let res = post_form_data(&action, form_data).await;
|
||||
match res {
|
||||
Err(e) => {
|
||||
error!("<Form/> error while POSTing: {e:#?}");
|
||||
if let Some(on_error) = on_error {
|
||||
on_error(&e);
|
||||
}
|
||||
if let Some(error) = error {
|
||||
error.try_set(Some(Box::new(e)));
|
||||
}
|
||||
}
|
||||
Ok(resp) => {
|
||||
let resp = web_sys::Response::from(resp);
|
||||
if let Some(version) = action_version {
|
||||
version.update(|n| *n += 1);
|
||||
}
|
||||
if let Some(error) = error {
|
||||
error.try_set(None);
|
||||
}
|
||||
if let Some(on_response) = on_response.clone() {
|
||||
on_response(&resp);
|
||||
}
|
||||
// Check all the logical 3xx responses that might
|
||||
// get returned from a server function
|
||||
if resp.redirected() {
|
||||
let resp_url = &resp.url();
|
||||
match Url::try_from(resp_url.as_str()) {
|
||||
Ok(url) => {
|
||||
if url.origin
|
||||
!= current_window_origin()
|
||||
|| navigate.is_none()
|
||||
{
|
||||
_ = window()
|
||||
.location()
|
||||
.set_href(
|
||||
resp_url.as_str(),
|
||||
);
|
||||
} else {
|
||||
#[allow(
|
||||
clippy::unnecessary_unwrap
|
||||
)]
|
||||
let navigate =
|
||||
navigate.unwrap();
|
||||
navigate(
|
||||
&format!(
|
||||
"{}{}{}",
|
||||
url.pathname,
|
||||
if url.search.is_empty()
|
||||
{
|
||||
""
|
||||
} else {
|
||||
"?"
|
||||
},
|
||||
url.search,
|
||||
),
|
||||
navigate_options,
|
||||
)
|
||||
}
|
||||
}
|
||||
Err(e) => warn!("{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// POST
|
||||
else if method == "post" {
|
||||
ev.prevent_default();
|
||||
ev.stop_propagation();
|
||||
|
||||
let on_response = on_response.clone();
|
||||
let on_error = on_error.clone();
|
||||
spawn_local(async move {
|
||||
let res = post_params(&action, &enctype, params).await;
|
||||
match res {
|
||||
Err(e) => {
|
||||
error!("<Form/> error while POSTing: {e:#?}");
|
||||
if let Some(on_error) = on_error {
|
||||
on_error(&e);
|
||||
}
|
||||
if let Some(error) = error {
|
||||
error.try_set(Some(Box::new(e)));
|
||||
}
|
||||
}
|
||||
Ok(resp) => {
|
||||
let resp = web_sys::Response::from(resp);
|
||||
if let Some(version) = action_version {
|
||||
version.update(|n| *n += 1);
|
||||
}
|
||||
if let Some(error) = error {
|
||||
error.try_set(None);
|
||||
}
|
||||
if let Some(on_response) = on_response.clone() {
|
||||
on_response(&resp);
|
||||
}
|
||||
// Check all the logical 3xx responses that might
|
||||
// get returned from a server function
|
||||
if resp.redirected() {
|
||||
let resp_url = &resp.url();
|
||||
match Url::try_from(resp_url.as_str()) {
|
||||
Ok(url) => {
|
||||
if url.origin
|
||||
!= current_window_origin()
|
||||
|| navigate.is_none()
|
||||
{
|
||||
_ = window()
|
||||
.location()
|
||||
.set_href(
|
||||
resp_url.as_str(),
|
||||
);
|
||||
} else {
|
||||
#[allow(
|
||||
clippy::unnecessary_unwrap
|
||||
)]
|
||||
let navigate =
|
||||
navigate.unwrap();
|
||||
navigate(
|
||||
&format!(
|
||||
"{}{}{}",
|
||||
url.pathname,
|
||||
if url.search.is_empty()
|
||||
{
|
||||
""
|
||||
} else {
|
||||
"?"
|
||||
},
|
||||
url.search,
|
||||
),
|
||||
navigate_options,
|
||||
)
|
||||
}
|
||||
}
|
||||
Err(e) => warn!("{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// otherwise, GET
|
||||
else {
|
||||
let params =
|
||||
params.to_string().as_string().unwrap_or_default();
|
||||
if let Some(navigate) = navigate {
|
||||
navigate(
|
||||
&format!("{action}?{params}"),
|
||||
navigate_options,
|
||||
);
|
||||
} else {
|
||||
_ = window()
|
||||
.location()
|
||||
.set_href(&format!("{action}?{params}"));
|
||||
}
|
||||
ev.prevent_default();
|
||||
ev.stop_propagation();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let method = method.unwrap_or("get");
|
||||
|
||||
let mut form = form()
|
||||
.attr("method", method)
|
||||
.attr("action", move || action.get())
|
||||
.attr("enctype", enctype)
|
||||
.on(ev::submit, on_submit)
|
||||
.attr("class", class)
|
||||
.child(children());
|
||||
if let Some(node_ref) = node_ref {
|
||||
form = form.node_ref(node_ref)
|
||||
};
|
||||
for (attr_name, attr_value) in attributes {
|
||||
form = form.attr(attr_name, attr_value);
|
||||
}
|
||||
form
|
||||
}
|
||||
|
||||
let has_router = has_router();
|
||||
let action = if has_router {
|
||||
use_resolved_path(move || action.to_href()())
|
||||
} else {
|
||||
create_memo(move |_| Some(action.to_href()()))
|
||||
};
|
||||
let class = class.map(|bx| bx.into_attribute_boxed());
|
||||
inner(
|
||||
has_router,
|
||||
method,
|
||||
action,
|
||||
enctype,
|
||||
version,
|
||||
error,
|
||||
on_form_data,
|
||||
on_response,
|
||||
on_error,
|
||||
class,
|
||||
children,
|
||||
node_ref,
|
||||
noscroll,
|
||||
replace,
|
||||
attributes,
|
||||
)
|
||||
}
|
||||
|
||||
fn current_window_origin() -> String {
|
||||
let location = window().location();
|
||||
let protocol = location.protocol().unwrap_or_default();
|
||||
let hostname = location.hostname().unwrap_or_default();
|
||||
let port = location.port().unwrap_or_default();
|
||||
format!(
|
||||
"{}//{}{}{}",
|
||||
protocol,
|
||||
hostname,
|
||||
if port.is_empty() { "" } else { ":" },
|
||||
port
|
||||
)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// ## Encoding
|
||||
/// **Note:** `<ActionForm/>` only works with server functions that use the
|
||||
/// default `Url` encoding. This is to ensure that `<ActionForm/>` works correctly
|
||||
/// both before and after WASM has loaded.
|
||||
///
|
||||
/// ## Complex Inputs
|
||||
/// Server function arguments that are structs with nested serializable fields
|
||||
/// should make use of indexing notation of `serde_qs`.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_router::*;
|
||||
///
|
||||
/// #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
/// struct HeftyData {
|
||||
/// first_name: String,
|
||||
/// last_name: String,
|
||||
/// }
|
||||
///
|
||||
/// #[component]
|
||||
/// fn ComplexInput() -> impl IntoView {
|
||||
/// let submit = Action::<VeryImportantFn, _>::server();
|
||||
///
|
||||
/// view! {
|
||||
/// <ActionForm action=submit>
|
||||
/// <input type="text" name="hefty_arg[first_name]" value="leptos"/>
|
||||
/// <input
|
||||
/// type="text"
|
||||
/// name="hefty_arg[last_name]"
|
||||
/// value="closures-everywhere"
|
||||
/// />
|
||||
/// <input type="submit"/>
|
||||
/// </ActionForm>
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #[server]
|
||||
/// async fn very_important_fn(
|
||||
/// hefty_arg: HeftyData,
|
||||
/// ) -> Result<(), ServerFnError> {
|
||||
/// assert_eq!(hefty_arg.first_name.as_str(), "leptos");
|
||||
/// assert_eq!(hefty_arg.last_name.as_str(), "closures-everywhere");
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[component]
|
||||
pub fn ActionForm<ServFn>(
|
||||
/// 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 [`using_server_fn`](leptos_server::Action::using_server_fn).
|
||||
action: Action<
|
||||
ServFn,
|
||||
Result<ServFn::Output, ServerFnError<ServFn::Error>>,
|
||||
>,
|
||||
/// Sets the `id` attribute on the underlying `<form>` tag
|
||||
#[prop(optional, into)]
|
||||
id: Option<AttributeValue>,
|
||||
/// Sets the `class` attribute on the underlying `<form>` tag, making it easier to style.
|
||||
#[prop(optional, into)]
|
||||
class: Option<AttributeValue>,
|
||||
|
||||
/// A [`NodeRef`] in which the `<form>` element should be stored.
|
||||
#[prop(optional)]
|
||||
node_ref: Option<NodeRef<html::Form>>,
|
||||
/// Arbitrary attributes to add to the `<form>`
|
||||
#[prop(attrs, optional)]
|
||||
attributes: Vec<(&'static str, Attribute)>,
|
||||
/// Component children; should include the HTML of the form elements.
|
||||
children: Children,
|
||||
) -> impl IntoView
|
||||
where
|
||||
ServFn: DeserializeOwned + ServerFn<InputEncoding = PostUrl> + 'static,
|
||||
<<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<
|
||||
ServFn::Error,
|
||||
>>::FormData: From<FormData>,
|
||||
{
|
||||
let has_router = has_router();
|
||||
if !has_router {
|
||||
_ = server_fn::redirect::set_redirect_hook(|loc: &str| {
|
||||
if let Some(url) = resolve_redirect_url(loc) {
|
||||
_ = window().location().set_href(&url.href());
|
||||
}
|
||||
});
|
||||
}
|
||||
let action_url = action.url().unwrap_or_else(|| {
|
||||
debug_warn!(
|
||||
"<ActionForm/> action needs a URL. Either use \
|
||||
create_server_action() or Action::using_server_fn()."
|
||||
);
|
||||
String::new()
|
||||
});
|
||||
let version = action.version();
|
||||
let value = action.value();
|
||||
|
||||
let class = class.map(|bx| bx.into_attribute_boxed());
|
||||
let id = id.map(|bx| bx.into_attribute_boxed());
|
||||
|
||||
let on_submit = {
|
||||
move |ev: SubmitEvent| {
|
||||
if ev.default_prevented() {
|
||||
return;
|
||||
}
|
||||
|
||||
// <button formmethod="dialog"> should *not* dispatch the action, but should be allowed to
|
||||
// just bubble up and close the <dialog> naturally
|
||||
let is_dialog = ev
|
||||
.submitter()
|
||||
.and_then(|el| el.get_attribute("formmethod"))
|
||||
.as_deref()
|
||||
== Some("dialog");
|
||||
if is_dialog {
|
||||
return;
|
||||
}
|
||||
|
||||
ev.prevent_default();
|
||||
|
||||
match ServFn::from_event(&ev) {
|
||||
Ok(new_input) => {
|
||||
action.dispatch(new_input);
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Error converting form field into server function \
|
||||
arguments: {err:?}"
|
||||
);
|
||||
batch(move || {
|
||||
value.set(Some(Err(ServerFnError::Serialization(
|
||||
err.to_string(),
|
||||
))));
|
||||
version.update(|n| *n += 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut action_form = form()
|
||||
.attr("action", action_url)
|
||||
.attr("method", "post")
|
||||
.attr("id", id)
|
||||
.attr("class", class)
|
||||
.on(ev::submit, on_submit)
|
||||
.child(children());
|
||||
if let Some(node_ref) = node_ref {
|
||||
action_form = action_form.node_ref(node_ref)
|
||||
};
|
||||
for (attr_name, attr_value) in attributes {
|
||||
action_form = action_form.attr(attr_name, attr_value);
|
||||
}
|
||||
action_form
|
||||
}
|
||||
|
||||
/// Automatically turns a server [MultiAction](leptos_server::MultiAction) into an HTML
|
||||
/// [`form`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)
|
||||
/// progressively enhanced to use client-side routing.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[component]
|
||||
pub fn MultiActionForm<ServFn>(
|
||||
/// 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].
|
||||
action: MultiAction<ServFn, Result<ServFn::Output, ServerFnError>>,
|
||||
/// Sets the `id` attribute on the underlying `<form>` tag
|
||||
#[prop(optional, into)]
|
||||
id: Option<AttributeValue>,
|
||||
/// Sets the `class` attribute on the underlying `<form>` tag, making it easier to style.
|
||||
#[prop(optional, into)]
|
||||
class: Option<AttributeValue>,
|
||||
/// A signal that will be set if the form submission ends in an error.
|
||||
#[prop(optional)]
|
||||
error: Option<RwSignal<Option<Box<dyn Error>>>>,
|
||||
/// A [`NodeRef`] in which the `<form>` element should be stored.
|
||||
#[prop(optional)]
|
||||
node_ref: Option<NodeRef<html::Form>>,
|
||||
/// Arbitrary attributes to add to the `<form>`
|
||||
#[prop(attrs, optional)]
|
||||
attributes: Vec<(&'static str, Attribute)>,
|
||||
/// Component children; should include the HTML of the form elements.
|
||||
children: Children,
|
||||
) -> impl IntoView
|
||||
where
|
||||
ServFn:
|
||||
Clone + DeserializeOwned + ServerFn<InputEncoding = PostUrl> + 'static,
|
||||
<<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<
|
||||
ServFn::Error,
|
||||
>>::FormData: From<FormData>,
|
||||
{
|
||||
let has_router = has_router();
|
||||
if !has_router {
|
||||
_ = server_fn::redirect::set_redirect_hook(|loc: &str| {
|
||||
if let Some(url) = resolve_redirect_url(loc) {
|
||||
_ = window().location().set_href(&url.href());
|
||||
}
|
||||
});
|
||||
}
|
||||
let action_url = action.url().unwrap_or_else(|| {
|
||||
debug_warn!(
|
||||
"<MultiActionForm/> action needs a URL. Either use \
|
||||
create_server_action() or Action::using_server_fn()."
|
||||
);
|
||||
String::new()
|
||||
});
|
||||
|
||||
let on_submit = move |ev: SubmitEvent| {
|
||||
if ev.default_prevented() {
|
||||
return;
|
||||
}
|
||||
|
||||
ev.prevent_default();
|
||||
|
||||
match ServFn::from_event(&ev) {
|
||||
Err(e) => {
|
||||
if let Some(error) = error {
|
||||
error.try_set(Some(Box::new(e)));
|
||||
}
|
||||
}
|
||||
Ok(input) => {
|
||||
action.dispatch(input);
|
||||
if let Some(error) = error {
|
||||
error.try_set(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let class = class.map(|bx| bx.into_attribute_boxed());
|
||||
|
||||
let id = id.map(|bx| bx.into_attribute_boxed());
|
||||
let mut action_form = form()
|
||||
.attr("action", action_url)
|
||||
.attr("method", "post")
|
||||
.attr("id", id)
|
||||
.attr("class", class)
|
||||
.on(ev::submit, on_submit)
|
||||
.child(children());
|
||||
if let Some(node_ref) = node_ref {
|
||||
action_form = action_form.node_ref(node_ref)
|
||||
};
|
||||
for (attr_name, attr_value) in attributes {
|
||||
action_form = action_form.attr(attr_name, attr_value);
|
||||
}
|
||||
action_form
|
||||
}
|
||||
|
||||
fn form_data_from_event(
|
||||
ev: &SubmitEvent,
|
||||
) -> Result<FormData, FromFormDataError> {
|
||||
let submitter = ev.submitter();
|
||||
let mut submitter_name_value = None;
|
||||
let opt_form = match &submitter {
|
||||
Some(el) => {
|
||||
if let Some(form) = el.dyn_ref::<HtmlFormElement>() {
|
||||
Some(form.clone())
|
||||
} else if let Some(input) = el.dyn_ref::<HtmlInputElement>() {
|
||||
submitter_name_value = Some((input.name(), input.value()));
|
||||
Some(ev.target().unwrap().unchecked_into())
|
||||
} else if let Some(button) = el.dyn_ref::<HtmlButtonElement>() {
|
||||
submitter_name_value = Some((button.name(), button.value()));
|
||||
Some(ev.target().unwrap().unchecked_into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
None => ev.target().map(|form| form.unchecked_into()),
|
||||
};
|
||||
match opt_form.as_ref().map(FormData::new_with_form) {
|
||||
None => Err(FromFormDataError::MissingForm(ev.clone().into())),
|
||||
Some(Err(e)) => Err(FromFormDataError::FormData(e)),
|
||||
Some(Ok(form_data)) => {
|
||||
if let Some((name, value)) = submitter_name_value {
|
||||
form_data
|
||||
.append_with_str(&name, &value)
|
||||
.map_err(FromFormDataError::FormData)?;
|
||||
}
|
||||
Ok(form_data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
fn extract_form_attributes(
|
||||
ev: &web_sys::Event,
|
||||
) -> (web_sys::HtmlFormElement, String, String, String) {
|
||||
let submitter = ev.unchecked_ref::<web_sys::SubmitEvent>().submitter();
|
||||
match &submitter {
|
||||
Some(el) => {
|
||||
if let Some(form) = el.dyn_ref::<web_sys::HtmlFormElement>() {
|
||||
(
|
||||
form.clone(),
|
||||
form.get_attribute("method")
|
||||
.unwrap_or_else(|| "get".to_string())
|
||||
.to_lowercase(),
|
||||
form.get_attribute("action")
|
||||
.unwrap_or_default()
|
||||
.to_lowercase(),
|
||||
form.get_attribute("enctype")
|
||||
.unwrap_or_else(|| {
|
||||
"application/x-www-form-urlencoded".to_string()
|
||||
})
|
||||
.to_lowercase(),
|
||||
)
|
||||
} else if let Some(input) =
|
||||
el.dyn_ref::<web_sys::HtmlInputElement>()
|
||||
{
|
||||
let form = ev
|
||||
.target()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::HtmlFormElement>();
|
||||
(
|
||||
form.clone(),
|
||||
input.get_attribute("method").unwrap_or_else(|| {
|
||||
form.get_attribute("method")
|
||||
.unwrap_or_else(|| "get".to_string())
|
||||
.to_lowercase()
|
||||
}),
|
||||
input.get_attribute("action").unwrap_or_else(|| {
|
||||
form.get_attribute("action")
|
||||
.unwrap_or_default()
|
||||
.to_lowercase()
|
||||
}),
|
||||
input.get_attribute("enctype").unwrap_or_else(|| {
|
||||
form.get_attribute("enctype")
|
||||
.unwrap_or_else(|| {
|
||||
"application/x-www-form-urlencoded".to_string()
|
||||
})
|
||||
.to_lowercase()
|
||||
}),
|
||||
)
|
||||
} else if let Some(button) =
|
||||
el.dyn_ref::<web_sys::HtmlButtonElement>()
|
||||
{
|
||||
let form = ev
|
||||
.target()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::HtmlFormElement>();
|
||||
(
|
||||
form.clone(),
|
||||
button.get_attribute("method").unwrap_or_else(|| {
|
||||
form.get_attribute("method")
|
||||
.unwrap_or_else(|| "get".to_string())
|
||||
.to_lowercase()
|
||||
}),
|
||||
button.get_attribute("action").unwrap_or_else(|| {
|
||||
form.get_attribute("action")
|
||||
.unwrap_or_default()
|
||||
.to_lowercase()
|
||||
}),
|
||||
button.get_attribute("enctype").unwrap_or_else(|| {
|
||||
form.get_attribute("enctype")
|
||||
.unwrap_or_else(|| {
|
||||
"application/x-www-form-urlencoded".to_string()
|
||||
})
|
||||
.to_lowercase()
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
leptos_dom::debug_warn!(
|
||||
"<Form/> cannot be submitted from a tag other than \
|
||||
<form>, <input>, or <button>"
|
||||
);
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
None => match ev.target() {
|
||||
None => {
|
||||
leptos_dom::debug_warn!(
|
||||
"<Form/> SubmitEvent fired without a target."
|
||||
);
|
||||
panic!()
|
||||
}
|
||||
Some(form) => {
|
||||
let form = form.unchecked_into::<web_sys::HtmlFormElement>();
|
||||
(
|
||||
form.clone(),
|
||||
form.get_attribute("method")
|
||||
.unwrap_or_else(|| "get".to_string()),
|
||||
form.get_attribute("action").unwrap_or_default(),
|
||||
form.get_attribute("enctype").unwrap_or_else(|| {
|
||||
"application/x-www-form-urlencoded".to_string()
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to deserialize a type from form data. This can be used for client-side
|
||||
/// validation during form submission.
|
||||
pub trait FromFormData
|
||||
where
|
||||
Self: Sized + serde::de::DeserializeOwned,
|
||||
{
|
||||
/// Tries to deserialize the data, given only the `submit` event.
|
||||
fn from_event(ev: &web_sys::Event) -> Result<Self, FromFormDataError>;
|
||||
|
||||
/// Tries to deserialize the data, given the actual form data.
|
||||
fn from_form_data(
|
||||
form_data: &web_sys::FormData,
|
||||
) -> Result<Self, serde_qs::Error>;
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum FromFormDataError {
|
||||
#[error("Could not find <form> connected to event.")]
|
||||
MissingForm(Event),
|
||||
#[error("Could not create FormData from <form>: {0:?}")]
|
||||
FormData(JsValue),
|
||||
#[error("Deserialization error: {0:?}")]
|
||||
Deserialization(serde_qs::Error),
|
||||
}
|
||||
|
||||
impl<T> FromFormData for T
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
{
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
fn from_event(ev: &Event) -> Result<Self, FromFormDataError> {
|
||||
let submit_ev = ev.unchecked_ref();
|
||||
let form_data = form_data_from_event(submit_ev)?;
|
||||
Self::from_form_data(&form_data)
|
||||
.map_err(FromFormDataError::Deserialization)
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
fn from_form_data(
|
||||
form_data: &web_sys::FormData,
|
||||
) -> Result<Self, serde_qs::Error> {
|
||||
let data =
|
||||
web_sys::UrlSearchParams::new_with_str_sequence_sequence(form_data)
|
||||
.unwrap_throw();
|
||||
let data = data.to_string().as_string().unwrap_or_default();
|
||||
serde_qs::Config::new(5, false).deserialize_str::<Self>(&data)
|
||||
}
|
||||
}
|
|
@ -1,255 +0,0 @@
|
|||
use crate::{use_location, use_resolved_path, State};
|
||||
use leptos::*;
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// 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 + '_>;
|
||||
}
|
||||
|
||||
impl ToHref for &str {
|
||||
fn to_href(&self) -> Box<dyn Fn() -> String> {
|
||||
let s = self.to_string();
|
||||
Box::new(move || s.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToHref for String {
|
||||
fn to_href(&self) -> Box<dyn Fn() -> String> {
|
||||
let s = self.clone();
|
||||
Box::new(move || s.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToHref for Cow<'_, str> {
|
||||
fn to_href(&self) -> Box<dyn Fn() -> String + '_> {
|
||||
let s = self.to_string();
|
||||
Box::new(move || s.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToHref for Oco<'_, str> {
|
||||
fn to_href(&self) -> Box<dyn Fn() -> String + '_> {
|
||||
let s = self.to_string();
|
||||
Box::new(move || s.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> ToHref for F
|
||||
where
|
||||
F: Fn() -> String + 'static,
|
||||
{
|
||||
fn to_href(&self) -> Box<dyn Fn() -> String + '_> {
|
||||
Box::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// An HTML [`a`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a)
|
||||
/// progressively enhanced to use client-side routing.
|
||||
///
|
||||
/// Client-side routing also works with ordinary HTML `<a>` tags, but `<A>` does two additional things:
|
||||
/// 1) Correctly resolves relative nested routes. Relative routing with ordinary `<a>` tags can be tricky.
|
||||
/// For example, if you have a route like `/post/:id`, `<A href="1">` will generate the correct relative
|
||||
/// route, but `<a href="1">` likely will not (depending on where it appears in your view.)
|
||||
/// 2) Sets the `aria-current` attribute if this link is the active link (i.e., it’s a link to the page you’re on).
|
||||
/// This is helpful for accessibility and for styling. For example, maybe you want to set the link a
|
||||
/// different color if it’s a link to the page you’re currently on.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[component]
|
||||
pub fn A<H>(
|
||||
/// Used to calculate the link's `href` attribute. Will be resolved relative
|
||||
/// to the current route.
|
||||
href: H,
|
||||
/// Where to display the linked URL, as the name for a browsing context (a tab, window, or `<iframe>`).
|
||||
#[prop(optional, into)]
|
||||
target: Option<Oco<'static, str>>,
|
||||
/// 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.
|
||||
#[prop(optional)]
|
||||
exact: bool,
|
||||
/// Provides a class to be added when the link is active. If provided, it will
|
||||
/// be added at the same time that the `aria-current` attribute is set.
|
||||
///
|
||||
/// This supports multiple space-separated class names.
|
||||
///
|
||||
/// **Performance**: If it’s possible to style the link using the CSS with the
|
||||
/// `[aria-current=page]` selector, you should prefer that, as it enables significant
|
||||
/// SSR optimizations.
|
||||
#[prop(optional, into)]
|
||||
active_class: Option<Oco<'static, str>>,
|
||||
/// An object of any type that will be pushed to router state
|
||||
#[prop(optional)]
|
||||
state: Option<State>,
|
||||
/// If `true`, the link will not add to the browser's history (so, pressing `Back`
|
||||
/// will skip this page.)
|
||||
#[prop(optional)]
|
||||
replace: bool,
|
||||
/// Sets the `class` attribute on the underlying `<a>` tag, making it easier to style.
|
||||
#[prop(optional, into)]
|
||||
class: Option<AttributeValue>,
|
||||
/// Sets the `id` attribute on the underlying `<a>` tag, making it easier to target.
|
||||
#[prop(optional, into)]
|
||||
id: Option<Oco<'static, str>>,
|
||||
/// Arbitrary attributes to add to the `<a>`. Attributes can be added with the
|
||||
/// `attr:` syntax in the `view` macro.
|
||||
#[prop(attrs)]
|
||||
attributes: Vec<(&'static str, Attribute)>,
|
||||
/// The nodes or elements to be shown inside the link.
|
||||
children: Children,
|
||||
) -> impl IntoView
|
||||
where
|
||||
H: ToHref + 'static,
|
||||
{
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
fn inner(
|
||||
href: Memo<Option<String>>,
|
||||
target: Option<Oco<'static, str>>,
|
||||
exact: bool,
|
||||
#[allow(unused)] state: Option<State>,
|
||||
#[allow(unused)] replace: bool,
|
||||
class: Option<AttributeValue>,
|
||||
#[allow(unused)] active_class: Option<Oco<'static, str>>,
|
||||
id: Option<Oco<'static, str>>,
|
||||
#[allow(unused)] attributes: Vec<(&'static str, Attribute)>,
|
||||
children: Children,
|
||||
) -> View {
|
||||
#[cfg(not(any(feature = "hydrate", feature = "csr")))]
|
||||
{
|
||||
_ = state;
|
||||
_ = replace;
|
||||
}
|
||||
|
||||
let location = use_location();
|
||||
let is_active = create_memo(move |_| {
|
||||
href.with(|href| {
|
||||
href.as_deref().is_some_and(|to| {
|
||||
let path = to
|
||||
.split(['?', '#'])
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.to_lowercase();
|
||||
location.pathname.with(|loc| {
|
||||
let loc = loc.to_lowercase();
|
||||
if exact {
|
||||
loc == path
|
||||
} else {
|
||||
std::iter::zip(loc.split('/'), path.split('/'))
|
||||
.all(|(loc_p, path_p)| loc_p == path_p)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
// if we have `active_class` or arbitrary attributes,
|
||||
// the SSR optimization doesn't play nicely
|
||||
// so we use the builder instead
|
||||
let needs_builder =
|
||||
active_class.is_some() || !attributes.is_empty();
|
||||
if needs_builder {
|
||||
let mut a = leptos::html::a()
|
||||
.attr("href", move || href.get().unwrap_or_default())
|
||||
.attr("target", target)
|
||||
.attr("aria-current", move || {
|
||||
if is_active.get() {
|
||||
Some("page")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.attr(
|
||||
"class",
|
||||
class.map(|class| class.into_attribute_boxed()),
|
||||
);
|
||||
|
||||
if let Some(active_class) = active_class {
|
||||
for class_name in active_class.split_ascii_whitespace()
|
||||
{
|
||||
a = a.class(class_name.to_string(), move || {
|
||||
is_active.get()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
a = a.attr("id", id).child(children());
|
||||
|
||||
for (attr_name, attr_value) in attributes {
|
||||
a = a.attr(attr_name, attr_value);
|
||||
}
|
||||
|
||||
a
|
||||
}
|
||||
// but keep the nice SSR optimization in most cases
|
||||
else {
|
||||
view! {
|
||||
<a
|
||||
href=move || href.get().unwrap_or_default()
|
||||
target=target
|
||||
aria-current=move || if is_active.get() { Some("page") } else { None }
|
||||
class=class
|
||||
id=id
|
||||
>
|
||||
{children()}
|
||||
</a>
|
||||
}
|
||||
}
|
||||
.into_view()
|
||||
}
|
||||
|
||||
// the non-SSR version doesn't need the SSR optimizations
|
||||
// DRY here to avoid WASM binary size bloat
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
{
|
||||
let mut a = view! {
|
||||
<a
|
||||
href=move || href.get().unwrap_or_default()
|
||||
target=target
|
||||
prop:state=state.map(|s| s.to_js_value())
|
||||
prop:replace=replace
|
||||
aria-current=move || if is_active.get() { Some("page") } else { None }
|
||||
class=class
|
||||
id=id
|
||||
>
|
||||
{children()}
|
||||
</a>
|
||||
};
|
||||
|
||||
if let Some(active_class) = active_class {
|
||||
for class_name in active_class.split_ascii_whitespace() {
|
||||
a = a.class(class_name.to_string(), move || is_active.get())
|
||||
}
|
||||
}
|
||||
|
||||
for (attr_name, attr_value) in attributes {
|
||||
a = a.attr(attr_name, attr_value);
|
||||
}
|
||||
|
||||
a.into_view()
|
||||
}
|
||||
}
|
||||
|
||||
let href = use_resolved_path(move || href.to_href()());
|
||||
inner(
|
||||
href,
|
||||
target,
|
||||
exact,
|
||||
state,
|
||||
replace,
|
||||
class,
|
||||
active_class,
|
||||
id,
|
||||
attributes,
|
||||
children,
|
||||
)
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
mod form;
|
||||
mod link;
|
||||
mod outlet;
|
||||
mod progress;
|
||||
mod redirect;
|
||||
mod route;
|
||||
mod router;
|
||||
mod routes;
|
||||
mod static_render;
|
||||
|
||||
pub use form::*;
|
||||
pub use link::*;
|
||||
pub use outlet::*;
|
||||
pub use progress::*;
|
||||
pub use redirect::*;
|
||||
pub use route::*;
|
||||
pub use router::*;
|
||||
pub use routes::*;
|
||||
pub use static_render::*;
|
|
@ -1,402 +0,0 @@
|
|||
use crate::{
|
||||
animation::{Animation, AnimationState},
|
||||
use_is_back_navigation, use_location, use_route, RouteContext,
|
||||
SetIsRouting,
|
||||
};
|
||||
use leptos::{leptos_dom::HydrationCtx, *};
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
use web_sys::AnimationEvent;
|
||||
|
||||
/// 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.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[component]
|
||||
pub fn Outlet() -> impl IntoView {
|
||||
_ = HydrationCtx::next_outlet();
|
||||
let id = HydrationCtx::id();
|
||||
let route = use_route();
|
||||
let route_states = expect_context::<Memo<crate::RouterState>>();
|
||||
|
||||
let child_id = create_memo({
|
||||
let route = route.clone();
|
||||
move |_| {
|
||||
route_states.track();
|
||||
route.child().map(|child| child.id())
|
||||
}
|
||||
});
|
||||
|
||||
let is_showing = Rc::new(Cell::new(None::<usize>));
|
||||
let (outlet, set_outlet) = create_signal(None::<View>);
|
||||
let build_outlet = as_child_of_current_owner(|child: RouteContext| {
|
||||
provide_context(child.clone());
|
||||
child.outlet().into_view()
|
||||
});
|
||||
create_isomorphic_effect(move |prev_disposer| {
|
||||
child_id.track();
|
||||
match (route.child(), &is_showing.get()) {
|
||||
(None, _) => {
|
||||
set_outlet.set(None);
|
||||
|
||||
// previous disposer will be dropped, and therefore disposed
|
||||
None
|
||||
}
|
||||
(Some(child), Some(is_showing_val))
|
||||
if child.id() == *is_showing_val =>
|
||||
{
|
||||
// do nothing: we don't need to rerender the component, because it's the same
|
||||
|
||||
// returning the disposer keeps it alive until the next iteration
|
||||
prev_disposer.flatten()
|
||||
}
|
||||
(Some(child), _) => {
|
||||
drop(prev_disposer);
|
||||
is_showing.set(Some(child.id()));
|
||||
let (outlet, disposer) = build_outlet(child);
|
||||
set_outlet.set(Some(outlet));
|
||||
// returning the disposer keeps it alive until the next iteration
|
||||
Some(disposer)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let outlet: Signal<Option<View>> =
|
||||
if cfg!(any(feature = "csr", feature = "hydrate"))
|
||||
&& use_context::<SetIsRouting>().is_some()
|
||||
{
|
||||
let global_suspense = expect_context::<GlobalSuspenseContext>();
|
||||
|
||||
let (current_view, set_current_view) = create_signal(None);
|
||||
|
||||
create_render_effect({
|
||||
move |prev| {
|
||||
let outlet = outlet.get();
|
||||
let is_fallback =
|
||||
!global_suspense.with_inner(|c| c.ready().get());
|
||||
if prev.is_none() {
|
||||
set_current_view.set(outlet);
|
||||
} else if !is_fallback {
|
||||
queue_microtask({
|
||||
let global_suspense = global_suspense.clone();
|
||||
move || {
|
||||
let is_fallback = untrack(move || {
|
||||
!global_suspense
|
||||
.with_inner(|c| c.ready().get())
|
||||
});
|
||||
if !is_fallback {
|
||||
set_current_view.set(outlet);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
current_view.into()
|
||||
} else {
|
||||
outlet.into()
|
||||
};
|
||||
|
||||
leptos::leptos_dom::DynChild::new_with_id(id, move || outlet.get())
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// ## Animations
|
||||
/// The router uses CSS classes for animations, and transitions to the next specified class in order when
|
||||
/// the `animationend` event fires. Each property takes a `&'static str` that can contain a class or classes
|
||||
/// to be added at certain points. These CSS classes must have associated animations.
|
||||
/// - `outro`: added when route is being unmounted
|
||||
/// - `start`: added when route is first created
|
||||
/// - `intro`: added after `start` has completed (if defined), and the route is being mounted
|
||||
/// - `finally`: added after the `intro` animation is complete
|
||||
///
|
||||
/// Each of these properties is optional, and the router will transition to the next correct state
|
||||
/// whenever an `animationend` event fires.
|
||||
#[component]
|
||||
pub fn AnimatedOutlet(
|
||||
/// Base classes to be applied to the `<div>` wrapping the outlet during any animation state.
|
||||
#[prop(optional, into)]
|
||||
class: Option<TextProp>,
|
||||
/// CSS class added when route is being unmounted
|
||||
#[prop(optional)]
|
||||
outro: Option<&'static str>,
|
||||
/// CSS class added when route is being unmounted, in a “back” navigation
|
||||
#[prop(optional)]
|
||||
outro_back: Option<&'static str>,
|
||||
/// CSS class added when route is first created
|
||||
#[prop(optional)]
|
||||
start: Option<&'static str>,
|
||||
/// CSS class added while the route is being mounted
|
||||
#[prop(optional)]
|
||||
intro: Option<&'static str>,
|
||||
/// CSS class added while the route is being mounted, in a “back” navigation
|
||||
#[prop(optional)]
|
||||
intro_back: Option<&'static str>,
|
||||
/// CSS class added after other animations have completed.
|
||||
#[prop(optional)]
|
||||
finally: Option<&'static str>,
|
||||
) -> impl IntoView {
|
||||
let pathname = use_location().pathname;
|
||||
let route = use_route();
|
||||
let is_showing = Rc::new(Cell::new(None::<usize>));
|
||||
let (outlet, set_outlet) = create_signal(None::<View>);
|
||||
let build_outlet = as_child_of_current_owner(|child: RouteContext| {
|
||||
provide_context(child.clone());
|
||||
child.outlet().into_view()
|
||||
});
|
||||
|
||||
let animation = Animation {
|
||||
outro,
|
||||
start,
|
||||
intro,
|
||||
finally,
|
||||
outro_back,
|
||||
intro_back,
|
||||
};
|
||||
let (animation_state, set_animation_state) =
|
||||
create_signal(AnimationState::Finally);
|
||||
let trigger_animation = create_rw_signal(());
|
||||
let is_back = use_is_back_navigation();
|
||||
let animation_and_outlet = create_memo({
|
||||
move |prev: Option<&(AnimationState, View)>| {
|
||||
let animation_state = animation_state.get();
|
||||
let next_outlet = outlet.get().unwrap_or_default();
|
||||
trigger_animation.track();
|
||||
match prev {
|
||||
None => (animation_state, next_outlet),
|
||||
Some((prev_state, prev_outlet)) => {
|
||||
let (next_state, can_advance) = animation
|
||||
.next_state(prev_state, is_back.get_untracked());
|
||||
|
||||
if can_advance {
|
||||
(next_state, next_outlet)
|
||||
} else {
|
||||
(next_state, prev_outlet.to_owned())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let current_animation = create_memo(move |_| animation_and_outlet.get().0);
|
||||
let current_outlet = create_memo(move |_| animation_and_outlet.get().1);
|
||||
|
||||
create_isomorphic_effect(move |prev_disposer| {
|
||||
pathname.track();
|
||||
|
||||
match (route.child(), &is_showing.get()) {
|
||||
(None, _) => {
|
||||
set_outlet.set(None);
|
||||
|
||||
// previous disposer will be dropped, and therefore disposed
|
||||
None
|
||||
}
|
||||
(Some(child), Some(is_showing_val))
|
||||
if child.id() == *is_showing_val =>
|
||||
{
|
||||
trigger_animation.set(());
|
||||
|
||||
// do nothing: we don't need to rerender the component, because it's the same
|
||||
// returning the disposer keeps it alive until the next iteration
|
||||
prev_disposer.flatten()
|
||||
}
|
||||
(Some(child), _) => {
|
||||
trigger_animation.set(());
|
||||
is_showing.set(Some(child.id()));
|
||||
let (outlet, disposer) = build_outlet(child);
|
||||
set_outlet.set(Some(outlet));
|
||||
// returning the disposer keeps it alive until the next iteration
|
||||
Some(disposer)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let class = move || {
|
||||
let animation_class = match current_animation.get() {
|
||||
AnimationState::Outro => outro.unwrap_or_default(),
|
||||
AnimationState::Start => start.unwrap_or_default(),
|
||||
AnimationState::Intro => intro.unwrap_or_default(),
|
||||
AnimationState::Finally => finally.unwrap_or_default(),
|
||||
AnimationState::OutroBack => outro_back.unwrap_or_default(),
|
||||
AnimationState::IntroBack => intro_back.unwrap_or_default(),
|
||||
};
|
||||
if let Some(class) = &class {
|
||||
format!("{} {animation_class}", class.get())
|
||||
} else {
|
||||
animation_class.to_string()
|
||||
}
|
||||
};
|
||||
let node_ref = create_node_ref::<html::Div>();
|
||||
let animationend = move |ev: AnimationEvent| {
|
||||
use wasm_bindgen::JsCast;
|
||||
if let Some(target) = ev.target() {
|
||||
let node_ref = node_ref.get();
|
||||
if node_ref.is_none()
|
||||
|| target
|
||||
.unchecked_ref::<web_sys::Node>()
|
||||
.is_same_node(Some(&*node_ref.unwrap()))
|
||||
{
|
||||
ev.stop_propagation();
|
||||
let current = current_animation.get();
|
||||
set_animation_state.update(|current_state| {
|
||||
let (next, _) =
|
||||
animation.next_state(¤t, is_back.get_untracked());
|
||||
*current_state = next;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
view! {
|
||||
<div class=class on:animationend=animationend>
|
||||
{move || current_outlet.get()}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
/// 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.
|
||||
///
|
||||
/// ## Animations
|
||||
/// The router uses CSS classes for animations, and transitions to the next specified class in order when
|
||||
/// the `animationend` event fires. Each property takes a `&'static str` that can contain a class or classes
|
||||
/// to be added at certain points. These CSS classes must have associated animations.
|
||||
/// - `outro`: added when route is being unmounted
|
||||
/// - `start`: added when route is first created
|
||||
/// - `intro`: added after `start` has completed (if defined), and the route is being mounted
|
||||
/// - `finally`: added after the `intro` animation is complete
|
||||
///
|
||||
/// Each of these properties is optional, and the router will transition to the next correct state
|
||||
/// whenever an `animationend` event fires.
|
||||
#[component]
|
||||
pub fn AnimatedOutlet(
|
||||
/// Base classes to be applied to the `<div>` wrapping the outlet during any animation state.
|
||||
#[prop(optional, into)]
|
||||
class: Option<TextProp>,
|
||||
/// CSS class added when route is being unmounted
|
||||
#[prop(optional)]
|
||||
outro: Option<&'static str>,
|
||||
/// CSS class added when route is being unmounted, in a “back” navigation
|
||||
#[prop(optional)]
|
||||
outro_back: Option<&'static str>,
|
||||
/// CSS class added when route is first created
|
||||
#[prop(optional)]
|
||||
start: Option<&'static str>,
|
||||
/// CSS class added while the route is being mounted
|
||||
#[prop(optional)]
|
||||
intro: Option<&'static str>,
|
||||
/// CSS class added while the route is being mounted, in a “back” navigation
|
||||
#[prop(optional)]
|
||||
intro_back: Option<&'static str>,
|
||||
/// CSS class added after other animations have completed.
|
||||
#[prop(optional)]
|
||||
finally: Option<&'static str>,
|
||||
) -> impl IntoView {
|
||||
let route = use_route();
|
||||
let is_showing = Rc::new(Cell::new(None::<usize>));
|
||||
let (outlet, set_outlet) = create_signal(None::<View>);
|
||||
|
||||
let animation = Animation {
|
||||
outro,
|
||||
start,
|
||||
intro,
|
||||
finally,
|
||||
outro_back,
|
||||
intro_back,
|
||||
};
|
||||
let (animation_state, set_animation_state) =
|
||||
create_signal(AnimationState::Finally);
|
||||
let trigger_animation = create_rw_signal(());
|
||||
let is_back = use_is_back_navigation();
|
||||
let animation_and_outlet = create_memo({
|
||||
move |prev: Option<&(AnimationState, View)>| {
|
||||
let animation_state = animation_state.get();
|
||||
let next_outlet = outlet.get().unwrap_or_default();
|
||||
trigger_animation.track();
|
||||
match prev {
|
||||
None => (animation_state, next_outlet),
|
||||
Some((prev_state, prev_outlet)) => {
|
||||
let (next_state, can_advance) = animation
|
||||
.next_state(prev_state, is_back.get_untracked());
|
||||
|
||||
if can_advance {
|
||||
(next_state, next_outlet)
|
||||
} else {
|
||||
(next_state, prev_outlet.to_owned())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let current_animation = create_memo(move |_| animation_and_outlet.get().0);
|
||||
let current_outlet = create_memo(move |_| animation_and_outlet.get().1);
|
||||
|
||||
create_isomorphic_effect(move |_| {
|
||||
match (route.child(), &is_showing.get()) {
|
||||
(None, prev) => {
|
||||
/* if let Some(prev_scope) = prev.map(|(_, scope)| scope) {
|
||||
prev_scope.dispose();
|
||||
} */
|
||||
set_outlet.set(None);
|
||||
}
|
||||
(Some(child), Some(is_showing_val))
|
||||
if child.id() == *is_showing_val =>
|
||||
{
|
||||
// do nothing: we don't need to rerender the component, because it's the same
|
||||
trigger_animation.set(());
|
||||
}
|
||||
(Some(child), prev) => {
|
||||
//provide_context(child_child.clone());
|
||||
set_outlet
|
||||
.set(Some(child.outlet().into_view()));
|
||||
is_showing.set(Some(child.id()));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let class = move || {
|
||||
let animation_class = match current_animation.get() {
|
||||
AnimationState::Outro => outro.unwrap_or_default(),
|
||||
AnimationState::Start => start.unwrap_or_default(),
|
||||
AnimationState::Intro => intro.unwrap_or_default(),
|
||||
AnimationState::Finally => finally.unwrap_or_default(),
|
||||
AnimationState::OutroBack => outro_back.unwrap_or_default(),
|
||||
AnimationState::IntroBack => intro_back.unwrap_or_default(),
|
||||
};
|
||||
if let Some(class) = &class {
|
||||
format!("{} {animation_class}", class.get())
|
||||
} else {
|
||||
animation_class.to_string()
|
||||
}
|
||||
};
|
||||
let node_ref = create_node_ref::<html::Div>();
|
||||
let animationend = move |ev: AnimationEvent| {
|
||||
use wasm_bindgen::JsCast;
|
||||
if let Some(target) = ev.target() {
|
||||
let node_ref = node_ref.get();
|
||||
if node_ref.is_none()
|
||||
|| target
|
||||
.unchecked_ref::<web_sys::Node>()
|
||||
.is_same_node(Some(&*node_ref.unwrap()))
|
||||
{
|
||||
ev.stop_propagation();
|
||||
let current = current_animation.get();
|
||||
set_animation_state.update(|current_state| {
|
||||
let (next, _) =
|
||||
animation.next_state(¤t, is_back.get_untracked());
|
||||
*current_state = next;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
view! {
|
||||
<div class=class on:animationend=animationend>
|
||||
{move || current_outlet.get()}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
*/
|
|
@ -1,68 +0,0 @@
|
|||
use leptos::{leptos_dom::helpers::IntervalHandle, *};
|
||||
|
||||
/// A visible indicator that the router is in the process of navigating
|
||||
/// to another route.
|
||||
///
|
||||
/// This is used when `<Router set_is_routing>` has been provided, to
|
||||
/// provide some visual indicator that the page is currently loading
|
||||
/// async data, so that it is does not appear to have frozen. It can be
|
||||
/// styled independently.
|
||||
#[component]
|
||||
pub fn RoutingProgress(
|
||||
/// Whether the router is currently loading the new page.
|
||||
#[prop(into)]
|
||||
is_routing: Signal<bool>,
|
||||
/// The maximum expected time for loading, which is used to
|
||||
/// calibrate the animation process.
|
||||
#[prop(optional, into)]
|
||||
max_time: std::time::Duration,
|
||||
/// The time to show the full progress bar after page has loaded, before hiding it. (Defaults to 100ms.)
|
||||
#[prop(default = std::time::Duration::from_millis(250))]
|
||||
before_hiding: std::time::Duration,
|
||||
/// CSS classes to be applied to the `<progress>`.
|
||||
#[prop(optional, into)]
|
||||
class: String,
|
||||
) -> impl IntoView {
|
||||
const INCREMENT_EVERY_MS: f32 = 5.0;
|
||||
let expected_increments =
|
||||
max_time.as_secs_f32() / (INCREMENT_EVERY_MS / 1000.0);
|
||||
let percent_per_increment = 100.0 / expected_increments;
|
||||
|
||||
let (is_showing, set_is_showing) = create_signal(false);
|
||||
let (progress, set_progress) = create_signal(0.0);
|
||||
|
||||
create_render_effect(move |prev: Option<Option<IntervalHandle>>| {
|
||||
if is_routing.get() && !is_showing.get() {
|
||||
set_is_showing.set(true);
|
||||
set_interval_with_handle(
|
||||
move || {
|
||||
set_progress.update(|n| *n += percent_per_increment);
|
||||
},
|
||||
std::time::Duration::from_millis(INCREMENT_EVERY_MS as u64),
|
||||
)
|
||||
.ok()
|
||||
} else if is_routing.get() && is_showing.get() {
|
||||
set_progress.set(0.0);
|
||||
prev?
|
||||
} else {
|
||||
set_progress.set(100.0);
|
||||
set_timeout(
|
||||
move || {
|
||||
set_progress.set(0.0);
|
||||
set_is_showing.set(false);
|
||||
},
|
||||
before_hiding,
|
||||
);
|
||||
if let Some(Some(interval)) = prev {
|
||||
interval.clear();
|
||||
}
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
view! {
|
||||
<Show when=move || is_showing.get() fallback=|| ()>
|
||||
<progress class=class.clone() min="0" max="100" value=move || progress.get()/>
|
||||
</Show>
|
||||
}
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
use crate::{use_navigate, use_resolved_path, NavigateOptions};
|
||||
use leptos::{
|
||||
component, provide_context, signal_prelude::*, use_context, IntoView,
|
||||
};
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Redirects the user to a new URL, whether on the client side or on the server
|
||||
/// side. If rendered on the server, this sets a `302` status code and sets a `Location`
|
||||
/// header. If rendered in the browser, it uses client-side navigation to redirect.
|
||||
/// In either case, it resolves the route relative to the current route. (To use
|
||||
/// an absolute path, prefix it with `/`).
|
||||
///
|
||||
/// **Note**: Support for server-side redirects is provided by the server framework
|
||||
/// integrations ([`leptos_actix`] and [`leptos_axum`]. If you’re not using one of those
|
||||
/// integrations, you should manually provide a way of redirecting on the server
|
||||
/// using [`provide_server_redirect`].
|
||||
///
|
||||
/// [`leptos_actix`]: <https://docs.rs/leptos_actix/>
|
||||
/// [`leptos_axum`]: <https://docs.rs/leptos_axum/>
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[component]
|
||||
pub fn Redirect<P>(
|
||||
/// The relative path to which the user should be redirected.
|
||||
path: P,
|
||||
/// Navigation options to be used on the client side.
|
||||
#[prop(optional)]
|
||||
#[allow(unused)]
|
||||
options: Option<NavigateOptions>,
|
||||
) -> impl IntoView
|
||||
where
|
||||
P: core::fmt::Display + 'static,
|
||||
{
|
||||
// resolve relative path
|
||||
let path = use_resolved_path(move || path.to_string());
|
||||
let path = path.get_untracked().unwrap_or_else(|| "/".to_string());
|
||||
|
||||
// redirect on the server
|
||||
if let Some(redirect_fn) = use_context::<ServerRedirectFunction>() {
|
||||
(redirect_fn.f)(&path);
|
||||
}
|
||||
// redirect on the client
|
||||
else {
|
||||
#[allow(unused)]
|
||||
let navigate = use_navigate();
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
navigate(&path, options.unwrap_or_default());
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
{
|
||||
leptos::logging::debug_warn!(
|
||||
"<Redirect/> is trying to redirect without \
|
||||
`ServerRedirectFunction` being provided. (If you’re getting \
|
||||
this on initial server start-up, it’s okay to ignore. It \
|
||||
just means that your root route is a redirect.)"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapping type for a function provided as context to allow for
|
||||
/// server-side redirects. See [`provide_server_redirect`]
|
||||
/// and [`Redirect`].
|
||||
#[derive(Clone)]
|
||||
pub struct ServerRedirectFunction {
|
||||
f: Rc<dyn Fn(&str)>,
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for ServerRedirectFunction {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("ServerRedirectFunction").finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides a function that can be used to redirect the user to another
|
||||
/// absolute path, on the server. This should set a `302` status code and an
|
||||
/// appropriate `Location` header.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub fn provide_server_redirect(handler: impl Fn(&str) + 'static) {
|
||||
provide_context(ServerRedirectFunction {
|
||||
f: Rc::new(handler),
|
||||
})
|
||||
}
|
|
@ -1,496 +0,0 @@
|
|||
use crate::{
|
||||
matching::{resolve_path, PathMatch, RouteDefinition, RouteMatch},
|
||||
ParamsMap, RouterContext, SsrMode, StaticData, StaticMode, StaticParamsMap,
|
||||
TrailingSlash,
|
||||
};
|
||||
use leptos::{leptos_dom::Transparent, *};
|
||||
use std::{
|
||||
any::Any,
|
||||
borrow::Cow,
|
||||
cell::{Cell, RefCell},
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
static ROUTE_ID: Cell<usize> = const { Cell::new(0) };
|
||||
}
|
||||
|
||||
// RouteDefinition.id is `pub` and required to be unique.
|
||||
// Should we make this public so users can generate unique IDs?
|
||||
pub(in crate::components) fn new_route_id() -> usize {
|
||||
ROUTE_ID.with(|id| {
|
||||
let next = id.get() + 1;
|
||||
id.set(next);
|
||||
next
|
||||
})
|
||||
}
|
||||
|
||||
/// Represents an HTTP method that can be handled by this route.
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
|
||||
pub enum Method {
|
||||
/// The [`GET`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET) method
|
||||
/// requests a representation of the specified resource.
|
||||
#[default]
|
||||
Get,
|
||||
/// The [`POST`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST) method
|
||||
/// submits an entity to the specified resource, often causing a change in
|
||||
/// state or side effects on the server.
|
||||
Post,
|
||||
/// The [`PUT`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT) method
|
||||
/// replaces all current representations of the target resource with the request payload.
|
||||
Put,
|
||||
/// The [`DELETE`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/DELETE) method
|
||||
/// deletes the specified resource.
|
||||
Delete,
|
||||
/// The [`PATCH`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH) method
|
||||
/// applies partial modifications to a resource.
|
||||
Patch,
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[component(transparent)]
|
||||
pub fn Route<E, F, P>(
|
||||
/// 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`).
|
||||
path: P,
|
||||
/// The view that should be shown when this route is matched. This can be any function
|
||||
/// that returns a type that implements [`IntoView`] (like `|| view! { <p>"Show this"</p> })`
|
||||
/// or `|| view! { <MyComponent/>` } or even, for a component with no props, `MyComponent`).
|
||||
view: F,
|
||||
/// The mode that this route prefers during server-side rendering. Defaults to out-of-order streaming.
|
||||
#[prop(optional)]
|
||||
ssr: SsrMode,
|
||||
/// The HTTP methods that this route can handle (defaults to only `GET`).
|
||||
#[prop(default = &[Method::Get])]
|
||||
methods: &'static [Method],
|
||||
/// A data-loading function that will be called when the route is matched. Its results can be
|
||||
/// accessed with [`use_route_data`](crate::use_route_data).
|
||||
#[prop(optional, into)]
|
||||
data: Option<Loader>,
|
||||
/// How this route should handle trailing slashes in its path.
|
||||
/// Overrides any setting applied to [`crate::components::Router`].
|
||||
/// Serves as a default for any inner Routes.
|
||||
#[prop(optional)]
|
||||
trailing_slash: Option<TrailingSlash>,
|
||||
/// `children` may be empty or include nested routes.
|
||||
#[prop(optional)]
|
||||
children: Option<Children>,
|
||||
) -> impl IntoView
|
||||
where
|
||||
E: IntoView,
|
||||
F: Fn() -> E + 'static,
|
||||
P: core::fmt::Display,
|
||||
{
|
||||
define_route(
|
||||
children,
|
||||
path.to_string(),
|
||||
Rc::new(move || view().into_view()),
|
||||
ssr,
|
||||
methods,
|
||||
data,
|
||||
None,
|
||||
None,
|
||||
trailing_slash,
|
||||
)
|
||||
}
|
||||
|
||||
/// Describes a route that is guarded by a certain condition. This works the same way as
|
||||
/// [`<Route/>`](Route), except that if the `condition` function evaluates to `false`, it
|
||||
/// redirects to `redirect_path` instead of displaying its `view`.
|
||||
///
|
||||
/// ## Reactive or Asynchronous Conditions
|
||||
///
|
||||
/// Note that the condition check happens once, at the time of navigation to the page. It
|
||||
/// is not reactive (i.e., it will not cause the user to navigate away from the page if the
|
||||
/// condition changes to `false`), which means it does not work well with asynchronous conditions.
|
||||
/// If you need to protect a route conditionally or via `Suspense`, you should used nested routing
|
||||
/// and wrap the condition around the `<Outlet/>`.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use leptos::*; use leptos_router::*;
|
||||
/// # if false {
|
||||
/// let has_permission = move || true; // TODO!
|
||||
///
|
||||
/// view! {
|
||||
/// <Routes>
|
||||
/// // parent route
|
||||
/// <Route path="/" view=move || {
|
||||
/// view! {
|
||||
/// // only show the outlet when `has_permission` is `true`, and hide it when it is `false`
|
||||
/// <Show when=move || has_permission() fallback=|| "Access denied!">
|
||||
/// <Outlet/>
|
||||
/// </Show>
|
||||
/// }
|
||||
/// }>
|
||||
/// // nested child route
|
||||
/// <Route path="/" view=|| view! { <p>"Protected data" </p> }/>
|
||||
/// </Route>
|
||||
/// </Routes>
|
||||
/// }
|
||||
/// # ;}
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[component(transparent)]
|
||||
pub fn ProtectedRoute<P, E, F, C>(
|
||||
/// 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`).
|
||||
path: P,
|
||||
/// The path that will be redirected to if the condition is `false`.
|
||||
redirect_path: P,
|
||||
/// Condition function that returns a boolean.
|
||||
condition: C,
|
||||
/// View that will be exposed if the condition is `true`.
|
||||
view: F,
|
||||
/// The mode that this route prefers during server-side rendering. Defaults to out-of-order streaming.
|
||||
#[prop(optional)]
|
||||
ssr: SsrMode,
|
||||
/// The HTTP methods that this route can handle (defaults to only `GET`).
|
||||
#[prop(default = &[Method::Get])]
|
||||
methods: &'static [Method],
|
||||
/// A data-loading function that will be called when the route is matched. Its results can be
|
||||
/// accessed with [`use_route_data`](crate::use_route_data).
|
||||
#[prop(optional, into)]
|
||||
data: Option<Loader>,
|
||||
/// How this route should handle trailing slashes in its path.
|
||||
/// Overrides any setting applied to [`crate::components::Router`].
|
||||
/// Serves as a default for any inner Routes.
|
||||
#[prop(optional)]
|
||||
trailing_slash: Option<TrailingSlash>,
|
||||
/// `children` may be empty or include nested routes.
|
||||
#[prop(optional)]
|
||||
children: Option<Children>,
|
||||
) -> impl IntoView
|
||||
where
|
||||
E: IntoView,
|
||||
F: Fn() -> E + 'static,
|
||||
P: core::fmt::Display + 'static,
|
||||
C: Fn() -> bool + 'static,
|
||||
{
|
||||
use crate::Redirect;
|
||||
let redirect_path = redirect_path.to_string();
|
||||
|
||||
define_route(
|
||||
children,
|
||||
path.to_string(),
|
||||
Rc::new(move || {
|
||||
if condition() {
|
||||
view().into_view()
|
||||
} else {
|
||||
view! { <Redirect path=redirect_path.clone()/> }.into_view()
|
||||
}
|
||||
}),
|
||||
ssr,
|
||||
methods,
|
||||
data,
|
||||
None,
|
||||
None,
|
||||
trailing_slash,
|
||||
)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[component(transparent)]
|
||||
pub fn StaticRoute<E, F, P, S>(
|
||||
/// 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`).
|
||||
path: P,
|
||||
/// The view that should be shown when this route is matched. This can be any function
|
||||
/// that returns a type that implements [IntoView] (like `|| view! { <p>"Show this"</p> })`
|
||||
/// or `|| view! { <MyComponent/>` } or even, for a component with no props, `MyComponent`).
|
||||
view: F,
|
||||
/// Creates a map of the params that should be built for a particular route.
|
||||
static_params: S,
|
||||
/// The static route mode
|
||||
#[prop(optional)]
|
||||
mode: StaticMode,
|
||||
/// A data-loading function that will be called when the route is matched. Its results can be
|
||||
/// accessed with [`use_route_data`](crate::use_route_data).
|
||||
#[prop(optional, into)]
|
||||
data: Option<Loader>,
|
||||
/// How this route should handle trailing slashes in its path.
|
||||
/// Overrides any setting applied to [`crate::components::Router`].
|
||||
/// Serves as a default for any inner Routes.
|
||||
#[prop(optional)]
|
||||
trailing_slash: Option<TrailingSlash>,
|
||||
/// `children` may be empty or include nested routes.
|
||||
#[prop(optional)]
|
||||
children: Option<Children>,
|
||||
) -> impl IntoView
|
||||
where
|
||||
E: IntoView,
|
||||
F: Fn() -> E + 'static,
|
||||
P: core::fmt::Display,
|
||||
S: Fn() -> Pin<Box<dyn Future<Output = StaticParamsMap> + Send + Sync>>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
{
|
||||
define_route(
|
||||
children,
|
||||
path.to_string(),
|
||||
Rc::new(move || view().into_view()),
|
||||
SsrMode::default(),
|
||||
&[Method::Get],
|
||||
data,
|
||||
Some(mode),
|
||||
Some(Arc::new(static_params)),
|
||||
trailing_slash,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn define_route(
|
||||
children: Option<Children>,
|
||||
path: String,
|
||||
view: Rc<dyn Fn() -> View>,
|
||||
ssr_mode: SsrMode,
|
||||
methods: &'static [Method],
|
||||
data: Option<Loader>,
|
||||
static_mode: Option<StaticMode>,
|
||||
static_params: Option<StaticData>,
|
||||
trailing_slash: Option<TrailingSlash>,
|
||||
) -> RouteDefinition {
|
||||
let children = children
|
||||
.map(|children| {
|
||||
children()
|
||||
.as_children()
|
||||
.iter()
|
||||
.filter_map(|child| {
|
||||
child
|
||||
.as_transparent()
|
||||
.and_then(|t| t.downcast_ref::<RouteDefinition>())
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
RouteDefinition {
|
||||
id: new_route_id(),
|
||||
path,
|
||||
children,
|
||||
view,
|
||||
ssr_mode,
|
||||
methods,
|
||||
data,
|
||||
static_mode,
|
||||
static_params,
|
||||
trailing_slash,
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoView for RouteDefinition {
|
||||
fn into_view(self) -> View {
|
||||
Transparent::new(self).into_view()
|
||||
}
|
||||
}
|
||||
|
||||
/// Context type that contains information about the current, matched route.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct RouteContext {
|
||||
pub(crate) inner: Rc<RouteContextInner>,
|
||||
}
|
||||
|
||||
impl RouteContext {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub(crate) fn new(
|
||||
router: &RouterContext,
|
||||
child: impl Fn() -> Option<RouteContext> + 'static,
|
||||
matcher: impl Fn() -> Option<RouteMatch> + 'static,
|
||||
) -> Option<Self> {
|
||||
let base = router.base();
|
||||
let base = base.path();
|
||||
let RouteMatch { path_match, route } = matcher()?;
|
||||
let PathMatch { path, .. } = path_match;
|
||||
let RouteDefinition {
|
||||
view: element,
|
||||
id,
|
||||
data,
|
||||
..
|
||||
} = route.key;
|
||||
let params = create_memo(move |_| {
|
||||
matcher()
|
||||
.map(|matched| matched.path_match.params)
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
let inner = Rc::new(RouteContextInner {
|
||||
id,
|
||||
base_path: base,
|
||||
child: Box::new(child),
|
||||
path: create_rw_signal(path),
|
||||
original_path: route.original_path.to_string(),
|
||||
params,
|
||||
outlet: Box::new(move || Some(element())),
|
||||
data: RefCell::new(None),
|
||||
});
|
||||
if let Some(loader) = data {
|
||||
let data = {
|
||||
let inner = Rc::clone(&inner);
|
||||
provide_context(RouteContext { inner });
|
||||
(loader.data)()
|
||||
};
|
||||
*inner.data.borrow_mut() = Some(data);
|
||||
}
|
||||
|
||||
Some(RouteContext { inner })
|
||||
}
|
||||
|
||||
pub(crate) fn id(&self) -> usize {
|
||||
self.inner.id
|
||||
}
|
||||
|
||||
/// Returns the URL path of the current route,
|
||||
/// including param values in their places.
|
||||
///
|
||||
/// e.g., this will return `/article/0` rather than `/article/:id`.
|
||||
/// For the opposite behavior, see [`RouteContext::original_path`].
|
||||
#[track_caller]
|
||||
pub fn path(&self) -> String {
|
||||
#[cfg(debug_assertions)]
|
||||
let caller = std::panic::Location::caller();
|
||||
|
||||
self.inner.path.try_get_untracked().unwrap_or_else(|| {
|
||||
leptos::logging::debug_warn!(
|
||||
"at {caller}, you call `.path()` on a `<Route/>` that has \
|
||||
already been disposed"
|
||||
);
|
||||
Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn set_path(&self, path: String) {
|
||||
self.inner.path.set(path);
|
||||
}
|
||||
|
||||
/// Returns the original URL path of the current route,
|
||||
/// with the param name rather than the matched parameter itself.
|
||||
///
|
||||
/// e.g., this will return `/article/:id` rather than `/article/0`
|
||||
/// For the opposite behavior, see [`RouteContext::path`].
|
||||
pub fn original_path(&self) -> &str {
|
||||
&self.inner.original_path
|
||||
}
|
||||
|
||||
/// A reactive wrapper for the route parameters that are currently matched.
|
||||
pub fn params(&self) -> Memo<ParamsMap> {
|
||||
self.inner.params
|
||||
}
|
||||
|
||||
pub(crate) fn base(path: &str, fallback: Option<fn() -> View>) -> Self {
|
||||
Self {
|
||||
inner: Rc::new(RouteContextInner {
|
||||
id: 0,
|
||||
base_path: path.to_string(),
|
||||
child: Box::new(|| None),
|
||||
path: create_rw_signal(path.to_string()),
|
||||
original_path: path.to_string(),
|
||||
params: create_memo(|_| ParamsMap::new()),
|
||||
outlet: Box::new(move || fallback.as_ref().map(move |f| f())),
|
||||
data: Default::default(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves a relative route, relative to the current route's path.
|
||||
pub fn resolve_path(&self, to: &str) -> Option<String> {
|
||||
resolve_path(
|
||||
&self.inner.base_path,
|
||||
to,
|
||||
Some(&self.inner.path.get_untracked()),
|
||||
)
|
||||
.map(String::from)
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_path_tracked(&self, to: &str) -> Option<String> {
|
||||
resolve_path(&self.inner.base_path, to, Some(&self.inner.path.get()))
|
||||
.map(Cow::into_owned)
|
||||
}
|
||||
|
||||
/// 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 IntoView {
|
||||
(self.inner.outlet)()
|
||||
}
|
||||
|
||||
/// The http method used to navigate to this route. Defaults to [`Method::Get`] when unavailable like in client side routing
|
||||
pub fn method(&self) -> Method {
|
||||
use_context().unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct RouteContextInner {
|
||||
base_path: String,
|
||||
pub(crate) id: usize,
|
||||
pub(crate) child: Box<dyn Fn() -> Option<RouteContext>>,
|
||||
pub(crate) path: RwSignal<String>,
|
||||
pub(crate) original_path: String,
|
||||
pub(crate) params: Memo<ParamsMap>,
|
||||
pub(crate) outlet: Box<dyn Fn() -> Option<View>>,
|
||||
pub(crate) data: RefCell<Option<Rc<dyn Any>>>,
|
||||
}
|
||||
|
||||
impl PartialEq for RouteContextInner {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.base_path == other.base_path
|
||||
&& self.path == other.path
|
||||
&& self.original_path == other.original_path
|
||||
&& self.params == other.params
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for RouteContextInner {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("RouteContextInner")
|
||||
.field("path", &self.path)
|
||||
.field("ParamsMap", &self.params)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Loader {
|
||||
pub(crate) data: Rc<dyn Fn() -> Rc<dyn Any>>,
|
||||
}
|
||||
|
||||
impl<F, T> From<F> for Loader
|
||||
where
|
||||
F: Fn() -> T + 'static,
|
||||
T: Any + Clone + 'static,
|
||||
{
|
||||
fn from(f: F) -> Self {
|
||||
Self {
|
||||
data: Rc::new(move || Rc::new(f())),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,581 +0,0 @@
|
|||
use crate::{
|
||||
create_location, matching::resolve_path, resolve_redirect_url,
|
||||
scroll_to_el, use_location, use_navigate, Branch, History, Location,
|
||||
LocationChange, RouteContext, RouterIntegrationContext, State,
|
||||
};
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
use crate::{unescape, Url};
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::{
|
||||
server_fn::{
|
||||
error::{ServerFnErrorSerde, ServerFnUrlError},
|
||||
redirect::RedirectHook,
|
||||
},
|
||||
*,
|
||||
};
|
||||
use send_wrapper::SendWrapper;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
use thiserror::Error;
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::UnwrapThrowExt;
|
||||
|
||||
/// Provides for client-side and server-side routing. This should usually be somewhere near
|
||||
/// the root of the application.
|
||||
#[component]
|
||||
pub fn Router(
|
||||
/// The base URL for the router. Defaults to `""`.
|
||||
#[prop(optional)]
|
||||
base: Option<&'static str>,
|
||||
/// A fallback that should be shown if no route is matched.
|
||||
#[prop(optional)]
|
||||
fallback: Option<fn() -> View>,
|
||||
/// A signal that will be set while the navigation process is underway.
|
||||
#[prop(optional, into)]
|
||||
set_is_routing: Option<SignalSetter<bool>>,
|
||||
/// How trailing slashes should be handled in [`Route`] paths.
|
||||
#[prop(optional)]
|
||||
trailing_slash: TrailingSlash,
|
||||
/// 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.
|
||||
children: Children,
|
||||
/// A unique identifier for this router, allowing you to mount multiple Leptos apps with
|
||||
/// different routes from the same server.
|
||||
#[prop(optional)]
|
||||
id: usize,
|
||||
) -> impl IntoView {
|
||||
// create a new RouterContext and provide it to every component beneath the router
|
||||
let router = RouterContext::new(id, base, fallback, trailing_slash);
|
||||
provide_context(router);
|
||||
provide_context(GlobalSuspenseContext::new());
|
||||
if let Some(set_is_routing) = set_is_routing {
|
||||
provide_context(SetIsRouting(set_is_routing));
|
||||
}
|
||||
|
||||
// set server function redirect hook
|
||||
let navigate = use_navigate();
|
||||
let navigate = SendWrapper::new(navigate);
|
||||
let router_hook = Box::new(move |loc: &str| {
|
||||
let Some(url) = resolve_redirect_url(loc) else {
|
||||
return; // resolve_redirect_url() already logs an error
|
||||
};
|
||||
let current_origin =
|
||||
leptos_dom::helpers::location().origin().unwrap_throw();
|
||||
if url.origin() == current_origin {
|
||||
let navigate = navigate.clone();
|
||||
// delay by a tick here, so that the Action updates *before* the redirect
|
||||
request_animation_frame(move || {
|
||||
navigate(&url.href(), Default::default());
|
||||
});
|
||||
// Use set_href() if the conditions for client-side navigation were not satisfied
|
||||
} else if let Err(e) =
|
||||
leptos_dom::helpers::location().set_href(&url.href())
|
||||
{
|
||||
leptos::logging::error!("Failed to redirect: {e:#?}");
|
||||
}
|
||||
}) as RedirectHook;
|
||||
_ = server_fn::redirect::set_redirect_hook(router_hook);
|
||||
|
||||
// provide ServerFnUrlError if it exists
|
||||
let location = use_location();
|
||||
if let (Some(path), Some(err)) = location
|
||||
.query
|
||||
.with_untracked(|q| (q.get("__path").cloned(), q.get("__err").cloned()))
|
||||
{
|
||||
let err: ServerFnError = ServerFnErrorSerde::de(&err);
|
||||
provide_context(Rc::new(ServerFnUrlError::new(path, err)))
|
||||
}
|
||||
|
||||
children()
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct SetIsRouting(pub SignalSetter<bool>);
|
||||
|
||||
/// Context type that contains information about the current router state.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RouterContext {
|
||||
pub(crate) inner: Rc<RouterContextInner>,
|
||||
}
|
||||
pub(crate) struct RouterContextInner {
|
||||
id: usize,
|
||||
pub location: Location,
|
||||
pub base: RouteContext,
|
||||
trailing_slash: TrailingSlash,
|
||||
pub possible_routes: RefCell<Option<Vec<Branch>>>,
|
||||
#[allow(unused)] // used in CSR/hydrate
|
||||
base_path: String,
|
||||
history: Box<dyn History>,
|
||||
reference: ReadSignal<String>,
|
||||
set_reference: WriteSignal<String>,
|
||||
referrers: Rc<RefCell<Vec<LocationChange>>>,
|
||||
state: ReadSignal<State>,
|
||||
set_state: WriteSignal<State>,
|
||||
pub(crate) is_back: RwSignal<bool>,
|
||||
pub(crate) path_stack: StoredValue<Vec<String>>,
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for RouterContextInner {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("RouterContextInner")
|
||||
.field("location", &self.location)
|
||||
.field("base", &self.base)
|
||||
.field("reference", &self.reference)
|
||||
.field("set_reference", &self.set_reference)
|
||||
.field("referrers", &self.referrers)
|
||||
.field("state", &self.state)
|
||||
.field("set_state", &self.set_state)
|
||||
.field("path_stack", &self.path_stack)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl RouterContext {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub(crate) fn new(
|
||||
id: usize,
|
||||
base: Option<&'static str>,
|
||||
fallback: Option<fn() -> View>,
|
||||
trailing_slash: TrailingSlash,
|
||||
) -> Self {
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
let history = use_context::<RouterIntegrationContext>()
|
||||
.unwrap_or_else(|| RouterIntegrationContext(Rc::new(crate::BrowserIntegration {})));
|
||||
} else {
|
||||
let history = use_context::<RouterIntegrationContext>().unwrap_or_else(|| {
|
||||
let msg = "No router integration found.\n\nIf you are using this in the browser, \
|
||||
you should enable `features = [\"csr\"]` or `features = [\"hydrate\"] in your \
|
||||
`leptos_router` import.\n\nIf you are using this on the server without a \
|
||||
Leptos server integration, you must call provide_context::<RouterIntegrationContext>(...) \
|
||||
somewhere above the <Router/>.";
|
||||
leptos::logging::debug_warn!("{}", msg);
|
||||
panic!("{}", msg);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Any `History` type gives a way to get a reactive signal of the current location
|
||||
// in the browser context, this is drawn from the `popstate` event
|
||||
// different server adapters can provide different `History` implementations to allow server routing
|
||||
let source = history.location();
|
||||
|
||||
// if initial route is empty, redirect to base path, if it exists
|
||||
let base = base.unwrap_or_default();
|
||||
let base_path = resolve_path("", base, None);
|
||||
|
||||
if let Some(base_path) = &base_path {
|
||||
if source.with_untracked(|s| s.value.is_empty()) {
|
||||
history.navigate(&LocationChange {
|
||||
value: base_path.to_string(),
|
||||
replace: true,
|
||||
scroll: false,
|
||||
state: State(None),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// the current URL
|
||||
let (reference, set_reference) =
|
||||
create_signal(source.with_untracked(|s| s.value.clone()));
|
||||
|
||||
// the current History.state
|
||||
let (state, set_state) =
|
||||
create_signal(source.with_untracked(|s| s.state.clone()));
|
||||
|
||||
// Each field of `location` reactively represents a different part of the current location
|
||||
let location = create_location(reference, state);
|
||||
let referrers: Rc<RefCell<Vec<LocationChange>>> =
|
||||
Rc::new(RefCell::new(Vec::new()));
|
||||
|
||||
// Create base route with fallback element
|
||||
let base_path = base_path.unwrap_or_default();
|
||||
let base = RouteContext::base(&base_path, fallback);
|
||||
|
||||
// Every time the History gives us a new location,
|
||||
// 1) start a transition
|
||||
// 2) update the reference (URL)
|
||||
// 3) update the state
|
||||
// this will trigger the new route match below
|
||||
|
||||
create_render_effect(move |_| {
|
||||
let LocationChange { value, state, .. } = source.get();
|
||||
untrack(move || {
|
||||
if value != reference.get() {
|
||||
set_reference.update(move |r| *r = value);
|
||||
set_state.update(move |s| *s = state);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let inner = Rc::new(RouterContextInner {
|
||||
id,
|
||||
base_path: base_path.into_owned(),
|
||||
path_stack: store_value(vec![location.pathname.get_untracked()]),
|
||||
location,
|
||||
base,
|
||||
trailing_slash,
|
||||
history: Box::new(history),
|
||||
|
||||
reference,
|
||||
set_reference,
|
||||
referrers,
|
||||
state,
|
||||
set_state,
|
||||
possible_routes: Default::default(),
|
||||
is_back: create_rw_signal(false),
|
||||
});
|
||||
|
||||
// handle all click events on anchor tags
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
{
|
||||
let click_event = leptos::window_event_listener_untyped("click", {
|
||||
let inner = Rc::clone(&inner);
|
||||
move |ev| inner.clone().handle_anchor_click(ev)
|
||||
});
|
||||
on_cleanup(move || click_event.remove());
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
pub(crate) fn id(&self) -> usize {
|
||||
self.inner.id
|
||||
}
|
||||
|
||||
pub(crate) fn trailing_slash(&self) -> TrailingSlash {
|
||||
self.inner.trailing_slash.clone()
|
||||
}
|
||||
|
||||
/// A list of all possible routes this router can match.
|
||||
pub fn possible_branches(&self) -> Vec<Branch> {
|
||||
self.inner
|
||||
.possible_routes
|
||||
.borrow()
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl RouterContextInner {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub(crate) fn navigate_from_route(
|
||||
self: Rc<Self>,
|
||||
to: &str,
|
||||
options: &NavigateOptions,
|
||||
) -> Result<(), NavigationError> {
|
||||
let this = Rc::clone(&self);
|
||||
|
||||
untrack(move || {
|
||||
let resolved_to = if options.resolve {
|
||||
this.base.resolve_path(to)
|
||||
} else {
|
||||
resolve_path("", to, None).map(String::from)
|
||||
};
|
||||
|
||||
// reset count of pending resources at global level
|
||||
if let Some(global) = use_context::<GlobalSuspenseContext>() {
|
||||
global.reset();
|
||||
}
|
||||
|
||||
match resolved_to {
|
||||
None => Err(NavigationError::NotRoutable(to.to_string())),
|
||||
Some(resolved_to) => {
|
||||
if self.referrers.borrow().len() > 32 {
|
||||
return Err(NavigationError::MaxRedirects);
|
||||
}
|
||||
|
||||
if resolved_to != this.reference.get()
|
||||
|| options.state != (this.state).get()
|
||||
{
|
||||
{
|
||||
self.referrers.borrow_mut().push(LocationChange {
|
||||
value: self.reference.get(),
|
||||
replace: options.replace,
|
||||
scroll: options.scroll,
|
||||
state: self.state.get(),
|
||||
});
|
||||
}
|
||||
let len = self.referrers.borrow().len();
|
||||
|
||||
let set_reference = self.set_reference;
|
||||
let set_state = self.set_state;
|
||||
let referrers = self.referrers.clone();
|
||||
let this = Rc::clone(&self);
|
||||
|
||||
let resolved = resolved_to.to_string();
|
||||
let state = options.state.clone();
|
||||
set_reference.update(move |r| *r = resolved);
|
||||
|
||||
set_state.update({
|
||||
let next_state = state.clone();
|
||||
move |state| *state = next_state
|
||||
});
|
||||
|
||||
let global_suspense =
|
||||
use_context::<GlobalSuspenseContext>();
|
||||
let path_stack = self.path_stack;
|
||||
let is_navigating_back = self.is_back.get_untracked();
|
||||
if !is_navigating_back {
|
||||
path_stack.update_value(|stack| {
|
||||
stack.push(resolved_to.clone())
|
||||
});
|
||||
}
|
||||
|
||||
let set_is_routing = use_context::<SetIsRouting>();
|
||||
if let Some(set_is_routing) = set_is_routing {
|
||||
set_is_routing.0.set(true);
|
||||
}
|
||||
spawn_local(async move {
|
||||
if let Some(set_is_routing) = set_is_routing {
|
||||
if let Some(global) = global_suspense {
|
||||
global.with_inner(|s| s.to_future()).await;
|
||||
}
|
||||
set_is_routing.0.set(false);
|
||||
}
|
||||
|
||||
if referrers.borrow().len() == len {
|
||||
this.navigate_end(LocationChange {
|
||||
value: resolved_to,
|
||||
replace: false,
|
||||
scroll: true,
|
||||
state,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
scroll_to_el(false);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn navigate_end(self: Rc<Self>, mut next: LocationChange) {
|
||||
let first = self.referrers.borrow().first().cloned();
|
||||
if let Some(first) = first {
|
||||
if next.value != first.value || next.state != first.state {
|
||||
next.replace = first.replace;
|
||||
next.scroll = first.scroll;
|
||||
self.history.navigate(&next);
|
||||
}
|
||||
self.referrers.borrow_mut().clear();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub(crate) fn handle_anchor_click(self: Rc<Self>, ev: web_sys::Event) {
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
let ev = ev.unchecked_into::<web_sys::MouseEvent>();
|
||||
if ev.default_prevented()
|
||||
|| ev.button() != 0
|
||||
|| ev.meta_key()
|
||||
|| ev.alt_key()
|
||||
|| ev.ctrl_key()
|
||||
|| ev.shift_key()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let composed_path = ev.composed_path();
|
||||
let mut a: Option<web_sys::HtmlAnchorElement> = None;
|
||||
for i in 0..composed_path.length() {
|
||||
if let Ok(el) = composed_path
|
||||
.get(i)
|
||||
.dyn_into::<web_sys::HtmlAnchorElement>()
|
||||
{
|
||||
a = Some(el);
|
||||
}
|
||||
}
|
||||
if let Some(a) = a {
|
||||
let href = a.href();
|
||||
let target = a.target();
|
||||
|
||||
// let browser handle this event if link has target,
|
||||
// or if it doesn't have href or state
|
||||
// TODO "state" is set as a prop, not an attribute
|
||||
if !target.is_empty()
|
||||
|| (href.is_empty() && !a.has_attribute("state"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let rel = a.get_attribute("rel").unwrap_or_default();
|
||||
let mut rel = rel.split([' ', '\t']);
|
||||
|
||||
// let browser handle event if it has rel=external or download
|
||||
if a.has_attribute("download") || rel.any(|p| p == "external") {
|
||||
return;
|
||||
}
|
||||
|
||||
let url = Url::try_from(href.as_str()).unwrap();
|
||||
let path_name = crate::history::unescape_minimal(&url.pathname);
|
||||
|
||||
// let browser handle this event if it leaves our domain
|
||||
// or our base path
|
||||
if url.origin
|
||||
!= leptos_dom::helpers::location().origin().unwrap_or_default()
|
||||
|| (!self.base_path.is_empty()
|
||||
&& !path_name.is_empty()
|
||||
&& !path_name
|
||||
.to_lowercase()
|
||||
.starts_with(&self.base_path.to_lowercase()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let to = path_name
|
||||
+ if url.search.is_empty() { "" } else { "?" }
|
||||
+ &unescape(&url.search)
|
||||
+ &unescape(&url.hash);
|
||||
let state =
|
||||
leptos_dom::helpers::get_property(a.unchecked_ref(), "state")
|
||||
.ok()
|
||||
.and_then(|value| {
|
||||
if value == JsValue::UNDEFINED {
|
||||
None
|
||||
} else {
|
||||
Some(value)
|
||||
}
|
||||
});
|
||||
|
||||
ev.prevent_default();
|
||||
|
||||
let replace =
|
||||
leptos_dom::helpers::get_property(a.unchecked_ref(), "replace")
|
||||
.ok()
|
||||
.and_then(|value| value.as_bool())
|
||||
.unwrap_or(false);
|
||||
if let Err(e) = self.navigate_from_route(
|
||||
&to,
|
||||
&NavigateOptions {
|
||||
resolve: false,
|
||||
replace,
|
||||
scroll: !a.has_attribute("noscroll"),
|
||||
state: State(state),
|
||||
},
|
||||
) {
|
||||
leptos::logging::error!("{e:#?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
impl Default for NavigateOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
resolve: true,
|
||||
replace: false,
|
||||
scroll: true,
|
||||
state: State(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Declares how you would like to handle trailing slashes in Route paths. This
|
||||
/// can be set on [`Router`] and overridden in [`crate::components::Route`]
|
||||
#[derive(Default, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum TrailingSlash {
|
||||
/// This is the default behavior as of Leptos 0.5. Trailing slashes in your
|
||||
/// `Route` path are stripped. i.e.: the following two route declarations
|
||||
/// are equivalent:
|
||||
/// * `<Route path="/foo">`
|
||||
/// * `<Route path="/foo/">`
|
||||
#[default]
|
||||
Drop,
|
||||
|
||||
/// This mode will respect your path as it is written. Ex:
|
||||
/// * If you specify `<Route path="/foo">`, then `/foo` matches, but
|
||||
/// `/foo/` does not.
|
||||
/// * If you specify `<Route path="/foo/">`, then `/foo/` matches, but
|
||||
/// `/foo` does not.
|
||||
Exact,
|
||||
|
||||
/// Like `Exact`, this mode respects your path as-written. But it will also
|
||||
/// add redirects to the specified path if a user nagivates to a URL that is
|
||||
/// off by only the trailing slash.
|
||||
///
|
||||
/// Given `<Route path="/foo">`
|
||||
/// * Visiting `/foo` is valid.
|
||||
/// * Visiting `/foo/` serves a redirect to `/foo`
|
||||
///
|
||||
/// Given `<Route path="/foo/">`
|
||||
/// * Visiting `/foo` serves a redirect to `/foo/`
|
||||
/// * Visiting `/foo/` is valid.
|
||||
Redirect,
|
||||
}
|
||||
|
||||
impl TrailingSlash {
|
||||
/// Should we redirect requests that come in with the wrong (extra/missing) trailng slash?
|
||||
pub(crate) fn should_redirect(&self) -> bool {
|
||||
use TrailingSlash::*;
|
||||
match self {
|
||||
Redirect => true,
|
||||
Drop | Exact => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_route_path(&self, path: &mut String) {
|
||||
if !self.should_drop() {
|
||||
return;
|
||||
}
|
||||
while path.ends_with('/') {
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
|
||||
fn should_drop(&self) -> bool {
|
||||
use TrailingSlash::*;
|
||||
match self {
|
||||
Redirect | Exact => false,
|
||||
Drop => true,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,790 +0,0 @@
|
|||
use crate::{
|
||||
animation::*,
|
||||
components::route::new_route_id,
|
||||
matching::{
|
||||
expand_optionals, get_route_matches, join_paths, Branch, Matcher,
|
||||
RouteDefinition, RouteMatch,
|
||||
},
|
||||
use_is_back_navigation, use_route, NavigateOptions, Redirect, RouteContext,
|
||||
RouterContext, SetIsRouting, TrailingSlash,
|
||||
};
|
||||
use leptos::{leptos_dom::HydrationCtx, *};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cell::{Cell, RefCell},
|
||||
cmp::Reverse,
|
||||
collections::HashMap,
|
||||
ops::IndexMut,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// **Note:** Your application should only include one `<Routes/>` or `<AnimatedRoutes/>` component.
|
||||
///
|
||||
/// You should not conditionally render `<Routes/>` using another component like `<Show/>` or `<Suspense/>`.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_router::*;
|
||||
/// # if false {
|
||||
/// // ❌ don't do this!
|
||||
/// view! {
|
||||
/// <Show when=|| 1 == 2 fallback=|| view! { <p>"Loading"</p> }>
|
||||
/// <Routes>
|
||||
/// <Route path="/" view=|| "Home"/>
|
||||
/// </Routes>
|
||||
/// </Show>
|
||||
/// }
|
||||
/// # ;}
|
||||
/// ```
|
||||
///
|
||||
/// Instead, you can use nested routing to render your `<Routes/>` once, and conditionally render the router outlet:
|
||||
///
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_router::*;
|
||||
/// # if false {
|
||||
/// // ✅ do this instead!
|
||||
/// view! {
|
||||
/// <Routes>
|
||||
/// // parent route
|
||||
/// <Route path="/" view=move || {
|
||||
/// view! {
|
||||
/// // only show the outlet if data have loaded
|
||||
/// <Show when=|| 1 == 2 fallback=|| view! { <p>"Loading"</p> }>
|
||||
/// <Outlet/>
|
||||
/// </Show>
|
||||
/// }
|
||||
/// }>
|
||||
/// // nested child route
|
||||
/// <Route path="/" view=|| "Home"/>
|
||||
/// </Route>
|
||||
/// </Routes>
|
||||
/// }
|
||||
/// # ;}
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[component]
|
||||
pub fn Routes(
|
||||
/// Base path relative at which the routes are mounted.
|
||||
#[prop(optional)]
|
||||
base: Option<String>,
|
||||
children: Children,
|
||||
) -> impl IntoView {
|
||||
let router = use_context::<RouterContext>()
|
||||
.expect("<Routes/> component should be nested within a <Router/>.");
|
||||
let router_id = router.id();
|
||||
|
||||
let base_route = router.base();
|
||||
let base = base.unwrap_or_default();
|
||||
|
||||
Branches::initialize(&router, &base, children());
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
if let Some(context) = use_context::<crate::PossibleBranchContext>() {
|
||||
Branches::with(router_id, &base, |branches| {
|
||||
*context.0.borrow_mut() = branches.to_vec()
|
||||
});
|
||||
}
|
||||
|
||||
let next_route = router.pathname();
|
||||
let current_route = next_route;
|
||||
|
||||
let root_equal = Rc::new(Cell::new(true));
|
||||
let route_states =
|
||||
route_states(router_id, base, &router, current_route, &root_equal);
|
||||
provide_context(route_states);
|
||||
|
||||
let id = HydrationCtx::id();
|
||||
let root_route =
|
||||
as_child_of_current_owner(move |(base_route, root_equal)| {
|
||||
root_route(base_route, route_states, root_equal)
|
||||
});
|
||||
let (root, dis) = root_route((base_route, root_equal));
|
||||
on_cleanup(move || drop(dis));
|
||||
|
||||
leptos::leptos_dom::DynChild::new_with_id(id, move || root.get())
|
||||
.into_view()
|
||||
}
|
||||
|
||||
/// Contains route definitions and manages the actual routing process, with animated transitions
|
||||
/// between routes.
|
||||
///
|
||||
/// You should locate the `<AnimatedRoutes/>` component wherever on the page you want the routes to appear.
|
||||
///
|
||||
/// ## Animations
|
||||
/// The router uses CSS classes for animations, and transitions to the next specified class in order when
|
||||
/// the `animationend` event fires. Each property takes a `&'static str` that can contain a class or classes
|
||||
/// to be added at certain points. These CSS classes must have associated animations.
|
||||
/// - `outro`: added when route is being unmounted
|
||||
/// - `start`: added when route is first created
|
||||
/// - `intro`: added after `start` has completed (if defined), and the route is being mounted
|
||||
/// - `finally`: added after the `intro` animation is complete
|
||||
///
|
||||
/// Each of these properties is optional, and the router will transition to the next correct state
|
||||
/// whenever an `animationend` event fires.
|
||||
///
|
||||
/// **Note:** Your application should only include one `<AnimatedRoutes/>` or `<Routes/>` component.
|
||||
#[component]
|
||||
pub fn AnimatedRoutes(
|
||||
/// Base classes to be applied to the `<div>` wrapping the routes during any animation state.
|
||||
#[prop(optional, into)]
|
||||
class: Option<TextProp>,
|
||||
/// Base path relative at which the routes are mounted.
|
||||
#[prop(optional)]
|
||||
base: Option<String>,
|
||||
/// CSS class added when route is being unmounted
|
||||
#[prop(optional)]
|
||||
outro: Option<&'static str>,
|
||||
/// CSS class added when route is being unmounted, in a “back” navigation
|
||||
#[prop(optional)]
|
||||
outro_back: Option<&'static str>,
|
||||
/// CSS class added when route is first created
|
||||
#[prop(optional)]
|
||||
start: Option<&'static str>,
|
||||
/// CSS class added while the route is being mounted
|
||||
#[prop(optional)]
|
||||
intro: Option<&'static str>,
|
||||
/// CSS class added while the route is being mounted, in a “back” navigation
|
||||
#[prop(optional)]
|
||||
intro_back: Option<&'static str>,
|
||||
/// CSS class added after other animations have completed.
|
||||
#[prop(optional)]
|
||||
finally: Option<&'static str>,
|
||||
children: Children,
|
||||
) -> impl IntoView {
|
||||
let router = use_context::<RouterContext>()
|
||||
.expect("<Routes/> component should be nested within a <Router/>.");
|
||||
let router_id = router.id();
|
||||
|
||||
let base_route = router.base();
|
||||
let base = base.unwrap_or_default();
|
||||
|
||||
Branches::initialize(&router, &base, children());
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
if let Some(context) = use_context::<crate::PossibleBranchContext>() {
|
||||
Branches::with(router_id, &base, |branches| {
|
||||
*context.0.borrow_mut() = branches.to_vec()
|
||||
});
|
||||
}
|
||||
|
||||
let animation = Animation {
|
||||
outro,
|
||||
start,
|
||||
intro,
|
||||
finally,
|
||||
outro_back,
|
||||
intro_back,
|
||||
};
|
||||
let is_back = use_is_back_navigation();
|
||||
let (animation_state, set_animation_state) =
|
||||
create_signal(AnimationState::Finally);
|
||||
let next_route = router.pathname();
|
||||
|
||||
let is_complete = Rc::new(Cell::new(true));
|
||||
let animation_and_route = create_memo({
|
||||
let is_complete = Rc::clone(&is_complete);
|
||||
let base = base.clone();
|
||||
|
||||
move |prev: Option<&(AnimationState, String)>| {
|
||||
let animation_state = animation_state.get();
|
||||
let next_route = next_route.get();
|
||||
let prev_matches = prev
|
||||
.map(|(_, r)| r)
|
||||
.cloned()
|
||||
.map(|location| get_route_matches(router_id, &base, location));
|
||||
let matches =
|
||||
get_route_matches(router_id, &base, next_route.clone());
|
||||
let same_route = prev_matches
|
||||
.and_then(|p| p.first().map(|r| r.route.key.clone()))
|
||||
== matches.first().map(|r| r.route.key.clone());
|
||||
if same_route {
|
||||
(animation_state, next_route)
|
||||
} else {
|
||||
match prev {
|
||||
None => (animation_state, next_route),
|
||||
Some((prev_state, prev_route)) => {
|
||||
let (next_state, can_advance) = animation
|
||||
.next_state(prev_state, is_back.get_untracked());
|
||||
|
||||
if can_advance || !is_complete.get() {
|
||||
(next_state, next_route)
|
||||
} else {
|
||||
(next_state, prev_route.to_owned())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let current_animation = create_memo(move |_| animation_and_route.get().0);
|
||||
let current_route = create_memo(move |_| animation_and_route.get().1);
|
||||
|
||||
let root_equal = Rc::new(Cell::new(true));
|
||||
let route_states =
|
||||
route_states(router_id, base, &router, current_route, &root_equal);
|
||||
|
||||
let root = root_route(base_route, route_states, root_equal);
|
||||
let node_ref = create_node_ref::<html::Div>();
|
||||
|
||||
html::div()
|
||||
.node_ref(node_ref)
|
||||
.attr("class", move || {
|
||||
let animation_class = match current_animation.get() {
|
||||
AnimationState::Outro => outro.unwrap_or_default(),
|
||||
AnimationState::Start => start.unwrap_or_default(),
|
||||
AnimationState::Intro => intro.unwrap_or_default(),
|
||||
AnimationState::Finally => finally.unwrap_or_default(),
|
||||
AnimationState::OutroBack => outro_back.unwrap_or_default(),
|
||||
AnimationState::IntroBack => intro_back.unwrap_or_default(),
|
||||
};
|
||||
is_complete.set(animation_class == finally.unwrap_or_default());
|
||||
if let Some(class) = &class {
|
||||
format!("{} {animation_class}", class.get())
|
||||
} else {
|
||||
animation_class.to_string()
|
||||
}
|
||||
})
|
||||
.on(leptos::ev::animationend, move |ev| {
|
||||
use wasm_bindgen::JsCast;
|
||||
if let Some(target) = ev.target() {
|
||||
if target
|
||||
.unchecked_ref::<web_sys::Node>()
|
||||
.is_same_node(Some(&*node_ref.get().unwrap()))
|
||||
{
|
||||
let current = current_animation.get();
|
||||
set_animation_state.update(|current_state| {
|
||||
let (next, _) = animation
|
||||
.next_state(¤t, is_back.get_untracked());
|
||||
*current_state = next;
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
.child(move || root.get())
|
||||
.into_view()
|
||||
}
|
||||
|
||||
pub(crate) struct Branches;
|
||||
|
||||
type BranchesCacheKey = (usize, Cow<'static, str>);
|
||||
thread_local! {
|
||||
static BRANCHES: RefCell<HashMap<BranchesCacheKey, Vec<Branch>>> = RefCell::new(HashMap::new());
|
||||
}
|
||||
|
||||
impl Branches {
|
||||
pub fn initialize(router: &RouterContext, base: &str, children: Fragment) {
|
||||
BRANCHES.with(|branches| {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
if cfg!(any(feature = "csr", feature = "hydrate"))
|
||||
&& !branches.borrow().is_empty()
|
||||
{
|
||||
leptos::logging::warn!(
|
||||
"You should only render the <Routes/> component once \
|
||||
in your app. Please see the docs at https://docs.rs/leptos_router/latest/leptos_router/fn.Routes.html."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut current = branches.borrow_mut();
|
||||
if !current.contains_key(&(router.id(), Cow::from(base))) {
|
||||
let mut branches = Vec::new();
|
||||
let mut children = children
|
||||
.as_children()
|
||||
.iter()
|
||||
.filter_map(|child| {
|
||||
let def = child
|
||||
.as_transparent()
|
||||
.and_then(|t| t.downcast_ref::<RouteDefinition>());
|
||||
if def.is_none() {
|
||||
leptos::logging::warn!(
|
||||
"[NOTE] The <Routes/> component should \
|
||||
include *only* <Route/>or <ProtectedRoute/> \
|
||||
components, or some \
|
||||
#[component(transparent)] that returns a \
|
||||
RouteDefinition."
|
||||
);
|
||||
}
|
||||
def
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
inherit_settings(&mut children, router);
|
||||
create_branches(
|
||||
&children,
|
||||
base,
|
||||
&mut Vec::new(),
|
||||
&mut branches,
|
||||
true,
|
||||
base,
|
||||
);
|
||||
current.insert((router.id(), Cow::Owned(base.into())), branches);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with<T>(
|
||||
router_id: usize,
|
||||
base: &str,
|
||||
cb: impl FnOnce(&[Branch]) -> T,
|
||||
) -> T {
|
||||
BRANCHES.with(|branches| {
|
||||
let branches = branches.borrow();
|
||||
let branches = branches.get(&(router_id, Cow::from(base))).expect(
|
||||
"Branches::initialize() should be called before \
|
||||
Branches::with()",
|
||||
);
|
||||
cb(branches)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// <Route>s may inherit settings from each other or <Router>.
|
||||
// This mutates RouteDefinitions to propagate those settings.
|
||||
fn inherit_settings(children: &mut [RouteDefinition], router: &RouterContext) {
|
||||
struct InheritProps {
|
||||
trailing_slash: Option<TrailingSlash>,
|
||||
}
|
||||
fn route_def_inherit(
|
||||
children: &mut [RouteDefinition],
|
||||
inherited: InheritProps,
|
||||
) {
|
||||
for child in children {
|
||||
if child.trailing_slash.is_none() {
|
||||
child.trailing_slash.clone_from(&inherited.trailing_slash);
|
||||
}
|
||||
route_def_inherit(
|
||||
&mut child.children,
|
||||
InheritProps {
|
||||
trailing_slash: child.trailing_slash.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
route_def_inherit(
|
||||
children,
|
||||
InheritProps {
|
||||
trailing_slash: Some(router.trailing_slash()),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn route_states(
|
||||
router_id: usize,
|
||||
base: String,
|
||||
router: &RouterContext,
|
||||
current_route: Memo<String>,
|
||||
root_equal: &Rc<Cell<bool>>,
|
||||
) -> Memo<RouterState> {
|
||||
// whenever path changes, update matches
|
||||
let matches = create_memo(move |_| {
|
||||
get_route_matches(router_id, &base, current_route.get())
|
||||
});
|
||||
|
||||
// iterate over the new matches, reusing old routes when they are the same
|
||||
// and replacing them with new routes when they differ
|
||||
let next: Rc<RefCell<Vec<RouteContext>>> = Default::default();
|
||||
let router = Rc::clone(&router.inner);
|
||||
|
||||
let owner =
|
||||
Owner::current().expect("<Routes/> created outside reactive system.");
|
||||
|
||||
create_memo({
|
||||
let root_equal = Rc::clone(root_equal);
|
||||
move |prev: Option<&RouterState>| {
|
||||
root_equal.set(true);
|
||||
next.borrow_mut().clear();
|
||||
|
||||
let next_matches = matches.get();
|
||||
let prev_matches = prev.as_ref().map(|p| &p.matches);
|
||||
let prev_routes = prev.as_ref().map(|p| &p.routes);
|
||||
|
||||
// are the new route matches the same as the previous route matches so far?
|
||||
let mut equal = prev_matches
|
||||
.map(|prev_matches| next_matches.len() == prev_matches.len())
|
||||
.unwrap_or(false);
|
||||
|
||||
for i in 0..next_matches.len() {
|
||||
let next = next.clone();
|
||||
let prev_match = prev_matches.and_then(|p| p.get(i));
|
||||
let next_match = next_matches.get(i).unwrap();
|
||||
|
||||
match (prev_routes, prev_match) {
|
||||
(Some(prev), Some(prev_match))
|
||||
if next_match.route.key == prev_match.route.key
|
||||
&& next_match.route.id == prev_match.route.id =>
|
||||
{
|
||||
let prev_one = { prev.borrow()[i].clone() };
|
||||
if next_match.path_match.path != prev_one.path() {
|
||||
prev_one
|
||||
.set_path(next_match.path_match.path.clone());
|
||||
}
|
||||
if i >= next.borrow().len() {
|
||||
next.borrow_mut().push(prev_one);
|
||||
} else {
|
||||
*(next.borrow_mut().index_mut(i)) = prev_one;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
equal = false;
|
||||
if i == 0 {
|
||||
root_equal.set(false);
|
||||
}
|
||||
|
||||
let next_ctx = with_owner(owner, {
|
||||
let next = Rc::clone(&next);
|
||||
let router = Rc::clone(&router);
|
||||
move || {
|
||||
RouteContext::new(
|
||||
&RouterContext { inner: router },
|
||||
move || {
|
||||
if let Some(route_states) =
|
||||
use_context::<Memo<RouterState>>()
|
||||
{
|
||||
route_states.with(|route_states| {
|
||||
let routes = route_states
|
||||
.routes
|
||||
.borrow();
|
||||
routes.get(i + 1).cloned()
|
||||
})
|
||||
} else {
|
||||
next.borrow().get(i + 1).cloned()
|
||||
}
|
||||
},
|
||||
move || matches.with(|m| m.get(i).cloned()),
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(next_ctx) = next_ctx {
|
||||
if next.borrow().len() > i + 1 {
|
||||
next.borrow_mut()[i] = next_ctx;
|
||||
} else {
|
||||
next.borrow_mut().push(next_ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(prev) = &prev {
|
||||
if equal {
|
||||
RouterState {
|
||||
matches: next_matches.to_vec(),
|
||||
routes: prev_routes.cloned().unwrap_or_default(),
|
||||
root: prev.root.clone(),
|
||||
}
|
||||
} else {
|
||||
let root = next.borrow().first().cloned();
|
||||
RouterState {
|
||||
matches: next_matches.to_vec(),
|
||||
routes: Rc::new(RefCell::new(next.borrow().to_vec())),
|
||||
root,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let root = next.borrow().first().cloned();
|
||||
RouterState {
|
||||
matches: next_matches.to_vec(),
|
||||
routes: Rc::new(RefCell::new(next.borrow().to_vec())),
|
||||
root,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn root_route(
|
||||
base_route: RouteContext,
|
||||
route_states: Memo<RouterState>,
|
||||
root_equal: Rc<Cell<bool>>,
|
||||
) -> Signal<Option<View>> {
|
||||
let root_disposer = RefCell::new(None);
|
||||
let outlet = as_child_of_current_owner(|route: RouteContext| {
|
||||
provide_context(route.clone());
|
||||
route.outlet().into_view()
|
||||
});
|
||||
let root_view = create_memo({
|
||||
let root_equal = Rc::clone(&root_equal);
|
||||
move |prev| {
|
||||
provide_context(route_states);
|
||||
route_states.with(|state| {
|
||||
if state.routes.borrow().is_empty() {
|
||||
let (outlet, disposer) = outlet(base_route.clone());
|
||||
drop(std::mem::replace(
|
||||
&mut *root_disposer.borrow_mut(),
|
||||
Some(disposer),
|
||||
));
|
||||
Some(outlet)
|
||||
} else {
|
||||
let root = state.routes.borrow();
|
||||
let root = root.first();
|
||||
|
||||
if prev.is_none() || !root_equal.get() {
|
||||
root.as_ref().map(|route| {
|
||||
drop(std::mem::take(
|
||||
&mut *root_disposer.borrow_mut(),
|
||||
));
|
||||
let (outlet, disposer) = outlet((*route).clone());
|
||||
*root_disposer.borrow_mut() = Some(disposer);
|
||||
outlet
|
||||
})
|
||||
} else {
|
||||
prev.cloned().unwrap()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
if cfg!(any(feature = "csr", feature = "hydrate"))
|
||||
&& use_context::<SetIsRouting>().is_some()
|
||||
{
|
||||
let global_suspense = expect_context::<GlobalSuspenseContext>();
|
||||
|
||||
let (current_view, set_current_view) = create_signal(None);
|
||||
|
||||
create_render_effect(move |prev| {
|
||||
let root = root_view.get();
|
||||
let is_fallback = !global_suspense.with_inner(|c| c.ready().get());
|
||||
if prev.is_none() {
|
||||
set_current_view.set(root);
|
||||
} else if !is_fallback {
|
||||
queue_microtask({
|
||||
let global_suspense = global_suspense.clone();
|
||||
move || {
|
||||
let is_fallback = untrack(move || {
|
||||
!global_suspense.with_inner(|c| c.ready().get())
|
||||
});
|
||||
if !is_fallback {
|
||||
set_current_view.set(root);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
current_view.into()
|
||||
} else {
|
||||
root_view.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct RouterState {
|
||||
matches: Vec<RouteMatch>,
|
||||
routes: Rc<RefCell<Vec<RouteContext>>>,
|
||||
root: Option<RouteContext>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct RouteData {
|
||||
// This ID is always the same as key.id. Deprecate?
|
||||
pub id: usize,
|
||||
pub key: RouteDefinition,
|
||||
pub pattern: String,
|
||||
pub original_path: String,
|
||||
pub matcher: Matcher,
|
||||
}
|
||||
|
||||
impl RouteData {
|
||||
fn score(&self) -> i32 {
|
||||
let (pattern, splat) = match self.pattern.split_once("/*") {
|
||||
Some((p, s)) => (p, Some(s)),
|
||||
None => (self.pattern.as_str(), None),
|
||||
};
|
||||
let segments = pattern
|
||||
.split('/')
|
||||
.filter(|n| !n.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
#[allow(clippy::bool_to_int_with_if)] // on the splat.is_none()
|
||||
segments.iter().fold(
|
||||
(segments.len() as i32) - if splat.is_none() { 0 } else { 1 },
|
||||
|score, segment| {
|
||||
score + if segment.starts_with(':') { 2 } else { 3 }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn create_branches(
|
||||
route_defs: &[RouteDefinition],
|
||||
base: &str,
|
||||
stack: &mut Vec<RouteData>,
|
||||
branches: &mut Vec<Branch>,
|
||||
static_valid: bool,
|
||||
parents_path: &str,
|
||||
) {
|
||||
for def in route_defs {
|
||||
let routes = create_routes(
|
||||
def,
|
||||
base,
|
||||
static_valid && def.static_mode.is_some(),
|
||||
parents_path,
|
||||
);
|
||||
for route in routes {
|
||||
stack.push(route.clone());
|
||||
|
||||
if def.children.is_empty() {
|
||||
let branch = create_branch(stack, branches.len());
|
||||
branches.push(branch);
|
||||
} else {
|
||||
create_branches(
|
||||
&def.children,
|
||||
&route.pattern,
|
||||
stack,
|
||||
branches,
|
||||
static_valid && route.key.static_mode.is_some(),
|
||||
&format!("{}{}", parents_path, def.path),
|
||||
);
|
||||
}
|
||||
|
||||
stack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
if stack.is_empty() {
|
||||
branches.sort_by_key(|branch| Reverse(branch.score));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn create_branch(routes: &[RouteData], index: usize) -> Branch {
|
||||
Branch {
|
||||
routes: routes.to_vec(),
|
||||
score: routes.last().unwrap().score() * 10000 - (index as i32),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
fn create_routes(
|
||||
route_def: &RouteDefinition,
|
||||
base: &str,
|
||||
static_valid: bool,
|
||||
parents_path: &str,
|
||||
) -> Vec<RouteData> {
|
||||
let RouteDefinition { children, .. } = route_def;
|
||||
let is_leaf = children.is_empty();
|
||||
if is_leaf && route_def.static_mode.is_some() && !static_valid {
|
||||
panic!(
|
||||
"Static rendering is not valid for route '{}{}', all parent \
|
||||
routes must also be statically renderable.",
|
||||
parents_path, route_def.path
|
||||
);
|
||||
}
|
||||
let trailing_slash = route_def
|
||||
.trailing_slash
|
||||
.clone()
|
||||
.expect("trailng_slash should be set by this point");
|
||||
let mut acc = Vec::new();
|
||||
for original_path in expand_optionals(&route_def.path) {
|
||||
let mut path = join_paths(base, &original_path).to_string();
|
||||
trailing_slash.normalize_route_path(&mut path);
|
||||
let pattern = if is_leaf {
|
||||
path
|
||||
} else if let Some((path, _splat)) = path.split_once("/*") {
|
||||
path.to_string()
|
||||
} else {
|
||||
path
|
||||
};
|
||||
|
||||
let route_data = RouteData {
|
||||
key: route_def.clone(),
|
||||
id: route_def.id,
|
||||
matcher: Matcher::new_with_partial(&pattern, !is_leaf),
|
||||
pattern,
|
||||
original_path: original_path.into_owned(),
|
||||
};
|
||||
|
||||
if route_data.matcher.is_wildcard() {
|
||||
// already handles trailing_slash
|
||||
} else if let Some(redirect_route) = redirect_route_for(route_def) {
|
||||
let pattern = &redirect_route.path;
|
||||
let redirect_route_data = RouteData {
|
||||
id: redirect_route.id,
|
||||
matcher: Matcher::new_with_partial(pattern, !is_leaf),
|
||||
pattern: pattern.to_owned(),
|
||||
original_path: pattern.to_owned(),
|
||||
key: redirect_route,
|
||||
};
|
||||
acc.push(redirect_route_data);
|
||||
}
|
||||
|
||||
acc.push(route_data);
|
||||
}
|
||||
acc
|
||||
}
|
||||
|
||||
/// A new route that redirects to `route` with the correct trailng slash.
|
||||
fn redirect_route_for(route: &RouteDefinition) -> Option<RouteDefinition> {
|
||||
if matches!(route.path.as_str(), "" | "/") {
|
||||
// Root paths are an exception to the rule and are always equivalent:
|
||||
return None;
|
||||
}
|
||||
|
||||
let trailing_slash = route
|
||||
.trailing_slash
|
||||
.clone()
|
||||
.expect("trailing_slash should be defined by now");
|
||||
|
||||
if !trailing_slash.should_redirect() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Are we creating a new route that adds or removes a slash?
|
||||
let add_slash = route.path.ends_with('/');
|
||||
let view = Rc::new(move || {
|
||||
view! {
|
||||
<FixTrailingSlash add_slash />
|
||||
}
|
||||
.into_view()
|
||||
});
|
||||
|
||||
let new_pattern = if add_slash {
|
||||
// If we need to add a slash, we need to match on the path w/o it:
|
||||
route.path.trim_end_matches('/').to_string()
|
||||
} else {
|
||||
format!("{}/", route.path)
|
||||
};
|
||||
let new_route = RouteDefinition {
|
||||
path: new_pattern,
|
||||
children: vec![],
|
||||
data: None,
|
||||
methods: route.methods,
|
||||
id: new_route_id(),
|
||||
view,
|
||||
ssr_mode: route.ssr_mode,
|
||||
static_mode: route.static_mode,
|
||||
static_params: None,
|
||||
trailing_slash: None, // Shouldn't be needed/used from here on out
|
||||
};
|
||||
|
||||
Some(new_route)
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn FixTrailingSlash(add_slash: bool) -> impl IntoView {
|
||||
let route = use_route();
|
||||
let path = if add_slash {
|
||||
format!("{}/", route.path())
|
||||
} else {
|
||||
route.path().trim_end_matches('/').to_string()
|
||||
};
|
||||
let options = NavigateOptions {
|
||||
replace: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
view! {
|
||||
<Redirect path options/>
|
||||
}
|
||||
}
|
|
@ -1,426 +0,0 @@
|
|||
#[cfg(feature = "ssr")]
|
||||
use crate::{RouteListing, RouterIntegrationContext, ServerIntegration};
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos::{create_runtime, provide_context, IntoView, LeptosOptions};
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos_meta::MetaContext;
|
||||
use linear_map::LinearMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(feature = "ssr")]
|
||||
use std::path::Path;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::Display,
|
||||
future::Future,
|
||||
hash::{Hash, Hasher},
|
||||
path::PathBuf,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
pub struct StaticParamsMap(pub LinearMap<String, Vec<String>>);
|
||||
|
||||
impl StaticParamsMap {
|
||||
/// Create a new empty `StaticParamsMap`.
|
||||
#[inline]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Insert a value into the map.
|
||||
#[inline]
|
||||
pub fn insert(&mut self, key: impl ToString, value: Vec<String>) {
|
||||
self.0.insert(key.to_string(), value);
|
||||
}
|
||||
|
||||
/// Get a value from the map.
|
||||
#[inline]
|
||||
pub fn get(&self, key: &str) -> Option<&Vec<String>> {
|
||||
self.0.get(key)
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug)]
|
||||
pub struct StaticPath<'b, 'a: 'b> {
|
||||
path: &'a str,
|
||||
segments: Vec<StaticPathSegment<'a>>,
|
||||
params: LinearMap<&'a str, &'b Vec<String>>,
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug)]
|
||||
enum StaticPathSegment<'a> {
|
||||
Static(&'a str),
|
||||
Param(&'a str),
|
||||
Wildcard(&'a str),
|
||||
}
|
||||
|
||||
impl<'b, 'a: 'b> StaticPath<'b, 'a> {
|
||||
pub fn new(path: &'a str) -> StaticPath<'b, 'a> {
|
||||
use StaticPathSegment::*;
|
||||
Self {
|
||||
path,
|
||||
segments: path
|
||||
.split('/')
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| match s.chars().next() {
|
||||
Some(':') => Param(&s[1..]),
|
||||
Some('*') => Wildcard(&s[1..]),
|
||||
_ => Static(s),
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
params: LinearMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_params(&mut self, params: &'b StaticParamsMap) {
|
||||
use StaticPathSegment::*;
|
||||
for segment in self.segments.iter() {
|
||||
match segment {
|
||||
Param(name) | Wildcard(name) => {
|
||||
if let Some(value) = params.get(name) {
|
||||
self.params.insert(name, value);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_paths(self) -> Vec<ResolvedStaticPath> {
|
||||
use StaticPathSegment::*;
|
||||
let mut paths = vec![ResolvedStaticPath(String::new())];
|
||||
|
||||
for segment in self.segments {
|
||||
match segment {
|
||||
Static(s) => {
|
||||
paths = paths
|
||||
.into_iter()
|
||||
.map(|p| ResolvedStaticPath(format!("{p}/{s}")))
|
||||
.collect::<Vec<_>>();
|
||||
}
|
||||
Param(name) | Wildcard(name) => {
|
||||
let mut new_paths = vec![];
|
||||
for path in paths {
|
||||
let Some(params) = self.params.get(name) else {
|
||||
panic!(
|
||||
"missing param {} for path: {}",
|
||||
name, self.path
|
||||
);
|
||||
};
|
||||
for val in params.iter() {
|
||||
new_paths.push(ResolvedStaticPath(format!(
|
||||
"{path}/{val}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
paths = new_paths;
|
||||
}
|
||||
}
|
||||
}
|
||||
paths
|
||||
}
|
||||
|
||||
pub fn parent(&self) -> Option<StaticPath<'b, 'a>> {
|
||||
if self.path == "/" || self.path.is_empty() {
|
||||
return None;
|
||||
}
|
||||
self.path
|
||||
.rfind('/')
|
||||
.map(|i| StaticPath::new(&self.path[..i]))
|
||||
}
|
||||
|
||||
pub fn parents(&self) -> Vec<StaticPath<'b, 'a>> {
|
||||
let mut parents = vec![];
|
||||
let mut parent = self.parent();
|
||||
while let Some(p) = parent {
|
||||
parent = p.parent();
|
||||
parents.push(p);
|
||||
}
|
||||
parents
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &'a str {
|
||||
self.path
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for StaticPath<'_, '_> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.path.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl StaticPath<'_, '_> {}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[repr(transparent)]
|
||||
pub struct ResolvedStaticPath(pub String);
|
||||
|
||||
impl Display for ResolvedStaticPath {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl ResolvedStaticPath {
|
||||
#[cfg(feature = "ssr")]
|
||||
pub async fn build<IV>(
|
||||
&self,
|
||||
options: &LeptosOptions,
|
||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||
additional_context: impl Fn() + 'static + Clone,
|
||||
) -> String
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let url = format!("http://leptos{self}");
|
||||
let app = {
|
||||
let app_fn = app_fn.clone();
|
||||
move || {
|
||||
provide_context(RouterIntegrationContext::new(
|
||||
ServerIntegration { path: url },
|
||||
));
|
||||
provide_context(MetaContext::new());
|
||||
(app_fn)().into_view()
|
||||
}
|
||||
};
|
||||
let (stream, runtime) = leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(app, move || "".into(), additional_context.clone());
|
||||
leptos_integration_utils::build_async_response(stream, options, runtime)
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub async fn write<IV>(
|
||||
&self,
|
||||
options: &LeptosOptions,
|
||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||
additional_context: impl Fn() + 'static + Clone,
|
||||
) -> Result<String, std::io::Error>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let html = self.build(options, app_fn, additional_context).await;
|
||||
let file_path = static_file_path(options, &self.0);
|
||||
let path = Path::new(&file_path);
|
||||
if let Some(path) = path.parent() {
|
||||
std::fs::create_dir_all(path)?
|
||||
}
|
||||
std::fs::write(path, &html)?;
|
||||
Ok(html)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub async fn build_static_routes<IV>(
|
||||
options: &LeptosOptions,
|
||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||
routes: &[RouteListing],
|
||||
static_data_map: &StaticDataMap,
|
||||
) -> Result<(), std::io::Error>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
build_static_routes_with_additional_context(
|
||||
options,
|
||||
app_fn,
|
||||
|| {},
|
||||
routes,
|
||||
static_data_map,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub async fn build_static_routes_with_additional_context<IV>(
|
||||
options: &LeptosOptions,
|
||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||
additional_context: impl Fn() + 'static + Clone,
|
||||
routes: &[RouteListing],
|
||||
static_data_map: &StaticDataMap,
|
||||
) -> Result<(), std::io::Error>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let mut static_data: HashMap<&str, StaticParamsMap> = HashMap::new();
|
||||
let runtime = create_runtime();
|
||||
additional_context();
|
||||
for (key, value) in static_data_map {
|
||||
match value {
|
||||
Some(value) => static_data.insert(key, value.as_ref()().await),
|
||||
None => static_data.insert(key, StaticParamsMap::default()),
|
||||
};
|
||||
}
|
||||
runtime.dispose();
|
||||
let static_routes = routes
|
||||
.iter()
|
||||
.filter(|route| route.static_mode().is_some())
|
||||
.collect::<Vec<_>>();
|
||||
// TODO: maybe make this concurrent in some capacity
|
||||
for route in static_routes {
|
||||
let mut path = StaticPath::new(route.leptos_path());
|
||||
for p in path.parents().into_iter().rev() {
|
||||
if let Some(data) = static_data.get(p.path()) {
|
||||
path.add_params(data);
|
||||
}
|
||||
}
|
||||
if let Some(data) = static_data.get(path.path()) {
|
||||
path.add_params(data);
|
||||
}
|
||||
#[allow(clippy::print_stdout)]
|
||||
for path in path.into_paths() {
|
||||
println!("building static route: {path}");
|
||||
path.write(options, app_fn.clone(), additional_context.clone())
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub type StaticData = Arc<StaticDataFn>;
|
||||
|
||||
pub type StaticDataFn = dyn Fn() -> Pin<Box<dyn Future<Output = StaticParamsMap> + Send + Sync>>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static;
|
||||
|
||||
pub type StaticDataMap = HashMap<String, Option<StaticData>>;
|
||||
|
||||
/// The mode to use when rendering the route statically.
|
||||
/// On mode `Upfront`, the route will be built with the server is started using the provided static
|
||||
/// data. On mode `Incremental`, the route will be built on the first request to it and then cached
|
||||
/// and returned statically for subsequent requests.
|
||||
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub enum StaticMode {
|
||||
#[default]
|
||||
Upfront,
|
||||
Incremental,
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub enum StaticStatusCode {
|
||||
Ok,
|
||||
NotFound,
|
||||
InternalServerError,
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub enum StaticResponse {
|
||||
ReturnResponse {
|
||||
body: String,
|
||||
status: StaticStatusCode,
|
||||
content_type: Option<&'static str>,
|
||||
},
|
||||
RenderDynamic,
|
||||
RenderNotFound,
|
||||
WriteFile {
|
||||
body: String,
|
||||
path: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[inline(always)]
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn static_file_path(options: &LeptosOptions, path: &str) -> String {
|
||||
let trimmed_path = path.trim_start_matches('/');
|
||||
let path = if trimmed_path.is_empty() {
|
||||
"index"
|
||||
} else {
|
||||
trimmed_path
|
||||
};
|
||||
format!("{}/{}.html", options.site_root, path)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[inline(always)]
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn not_found_path(options: &LeptosOptions) -> String {
|
||||
format!("{}{}.html", options.site_root, options.not_found_path)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[inline(always)]
|
||||
pub fn upfront_static_route(
|
||||
res: Result<String, std::io::Error>,
|
||||
) -> StaticResponse {
|
||||
match res {
|
||||
Ok(body) => StaticResponse::ReturnResponse {
|
||||
body,
|
||||
status: StaticStatusCode::Ok,
|
||||
content_type: Some("text/html"),
|
||||
},
|
||||
Err(e) => match e.kind() {
|
||||
std::io::ErrorKind::NotFound => StaticResponse::RenderNotFound,
|
||||
_ => {
|
||||
tracing::error!("error reading file: {}", e);
|
||||
StaticResponse::ReturnResponse {
|
||||
body: "Internal Server Error".into(),
|
||||
status: StaticStatusCode::InternalServerError,
|
||||
content_type: None,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[inline(always)]
|
||||
pub fn not_found_page(res: Result<String, std::io::Error>) -> StaticResponse {
|
||||
match res {
|
||||
Ok(body) => StaticResponse::ReturnResponse {
|
||||
body,
|
||||
status: StaticStatusCode::NotFound,
|
||||
content_type: Some("text/html"),
|
||||
},
|
||||
Err(e) => match e.kind() {
|
||||
std::io::ErrorKind::NotFound => StaticResponse::ReturnResponse {
|
||||
body: "Not Found".into(),
|
||||
status: StaticStatusCode::Ok,
|
||||
content_type: None,
|
||||
},
|
||||
_ => {
|
||||
tracing::error!("error reading not found file: {}", e);
|
||||
StaticResponse::ReturnResponse {
|
||||
body: "Internal Server Error".into(),
|
||||
status: StaticStatusCode::InternalServerError,
|
||||
content_type: None,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn incremental_static_route(
|
||||
res: Result<String, std::io::Error>,
|
||||
) -> StaticResponse {
|
||||
match res {
|
||||
Ok(body) => StaticResponse::ReturnResponse {
|
||||
body,
|
||||
status: StaticStatusCode::Ok,
|
||||
content_type: Some("text/html"),
|
||||
},
|
||||
Err(_) => StaticResponse::RenderDynamic,
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "ssr")]
|
||||
pub async fn render_dynamic<IV>(
|
||||
path: &str,
|
||||
options: &LeptosOptions,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
) -> StaticResponse
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let body = ResolvedStaticPath(path.into())
|
||||
.build(options, app_fn, additional_context)
|
||||
.await;
|
||||
let path = Path::new(&static_file_path(options, path)).into();
|
||||
StaticResponse::WriteFile { body, path }
|
||||
}
|
|
@ -1,204 +0,0 @@
|
|||
mod test_extract_routes;
|
||||
|
||||
use crate::{
|
||||
provide_server_redirect, Branch, Method, RouterIntegrationContext,
|
||||
ServerIntegration, SsrMode, StaticDataMap, StaticMode, StaticParamsMap,
|
||||
StaticPath,
|
||||
};
|
||||
use leptos::*;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::{HashMap, HashSet},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
/// Context to contain all possible routes.
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct PossibleBranchContext(pub(crate) Rc<RefCell<Vec<Branch>>>);
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
/// A route that this application can serve.
|
||||
pub struct RouteListing {
|
||||
path: String,
|
||||
leptos_path: String,
|
||||
mode: SsrMode,
|
||||
methods: HashSet<Method>,
|
||||
static_mode: Option<StaticMode>,
|
||||
}
|
||||
|
||||
impl RouteListing {
|
||||
/// Create a route listing from its parts.
|
||||
pub fn new(
|
||||
path: impl ToString,
|
||||
leptos_path: impl ToString,
|
||||
mode: SsrMode,
|
||||
methods: impl IntoIterator<Item = Method>,
|
||||
static_mode: Option<StaticMode>,
|
||||
) -> Self {
|
||||
Self {
|
||||
path: path.to_string(),
|
||||
leptos_path: leptos_path.to_string(),
|
||||
mode,
|
||||
methods: methods.into_iter().collect(),
|
||||
static_mode,
|
||||
}
|
||||
}
|
||||
|
||||
/// The path this route handles.
|
||||
///
|
||||
/// This should be formatted for whichever web server integegration is being used. (ex: leptos-actix.)
|
||||
/// When returned from leptos-router, it matches `self.leptos_path()`.
|
||||
pub fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
/// The leptos-formatted path this route handles.
|
||||
pub fn leptos_path(&self) -> &str {
|
||||
&self.leptos_path
|
||||
}
|
||||
|
||||
/// The rendering mode for this path.
|
||||
pub fn mode(&self) -> SsrMode {
|
||||
self.mode
|
||||
}
|
||||
|
||||
/// The HTTP request methods this path can handle.
|
||||
pub fn methods(&self) -> impl Iterator<Item = Method> + '_ {
|
||||
self.methods.iter().copied()
|
||||
}
|
||||
|
||||
/// Whether this route is statically rendered.
|
||||
#[inline(always)]
|
||||
pub fn static_mode(&self) -> Option<StaticMode> {
|
||||
self.static_mode
|
||||
}
|
||||
|
||||
/// Build a route statically, will return `Ok(true)` on success or `Ok(false)` when the route
|
||||
/// is not marked as statically rendered. All route parameters to use when resolving all paths
|
||||
/// to render should be passed in the `params` argument.
|
||||
pub async fn build_static<IV>(
|
||||
&self,
|
||||
options: &LeptosOptions,
|
||||
app_fn: impl Fn() -> IV + Send + 'static + Clone,
|
||||
additional_context: impl Fn() + Send + 'static + Clone,
|
||||
params: &StaticParamsMap,
|
||||
) -> Result<bool, std::io::Error>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
match self.static_mode {
|
||||
None => Ok(false),
|
||||
Some(_) => {
|
||||
let mut path = StaticPath::new(&self.leptos_path);
|
||||
path.add_params(params);
|
||||
for path in path.into_paths() {
|
||||
path.write(
|
||||
options,
|
||||
app_fn.clone(),
|
||||
additional_context.clone(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a list of all routes this application could possibly serve. This returns the raw routes in the leptos_router
|
||||
/// format. Odds are you want `generate_route_list()` from either the [`actix`] or [`axum`] integrations if you want
|
||||
/// to work with their router.
|
||||
///
|
||||
/// [`actix`]: <https://docs.rs/actix/>
|
||||
/// [`axum`]: <https://docs.rs/axum/>
|
||||
pub fn generate_route_list_inner<IV>(
|
||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||
) -> (Vec<RouteListing>, StaticDataMap)
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
generate_route_list_inner_with_context(app_fn, || {})
|
||||
}
|
||||
/// Generates a list of all routes this application could possibly serve. This returns the raw routes in the leptos_router
|
||||
/// format. Odds are you want `generate_route_list()` from either the [`actix`] or [`axum`] integrations if you want
|
||||
/// to work with their router.
|
||||
///
|
||||
/// [`actix`]: <https://docs.rs/actix/>
|
||||
/// [`axum`]: <https://docs.rs/axum/>
|
||||
pub fn generate_route_list_inner_with_context<IV>(
|
||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||
additional_context: impl Fn() + 'static + Clone,
|
||||
) -> (Vec<RouteListing>, StaticDataMap)
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let runtime = create_runtime();
|
||||
|
||||
let branches = get_branches(app_fn, additional_context);
|
||||
let branches = branches.0.borrow();
|
||||
|
||||
let mut static_data_map: StaticDataMap = HashMap::new();
|
||||
let routes = branches
|
||||
.iter()
|
||||
.flat_map(|branch| {
|
||||
let mode = branch
|
||||
.routes
|
||||
.iter()
|
||||
.map(|route| route.key.ssr_mode)
|
||||
.max()
|
||||
.unwrap_or_default();
|
||||
let methods = branch
|
||||
.routes
|
||||
.iter()
|
||||
.flat_map(|route| route.key.methods)
|
||||
.copied()
|
||||
.collect::<HashSet<_>>();
|
||||
let route = branch
|
||||
.routes
|
||||
.last()
|
||||
.map(|route| (route.key.static_mode, route.pattern.clone()));
|
||||
for route in branch.routes.iter() {
|
||||
static_data_map.insert(
|
||||
route.pattern.to_string(),
|
||||
route.key.static_params.clone(),
|
||||
);
|
||||
}
|
||||
route.map(|(static_mode, path)| RouteListing {
|
||||
leptos_path: path.clone(),
|
||||
path,
|
||||
mode,
|
||||
methods: methods.clone(),
|
||||
static_mode,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
runtime.dispose();
|
||||
(routes, static_data_map)
|
||||
}
|
||||
|
||||
fn get_branches<IV>(
|
||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||
additional_context: impl Fn() + 'static + Clone,
|
||||
) -> PossibleBranchContext
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let integration = ServerIntegration {
|
||||
path: "http://leptos.rs/".to_string(),
|
||||
};
|
||||
|
||||
provide_context(RouterIntegrationContext::new(integration));
|
||||
let branches = PossibleBranchContext::default();
|
||||
provide_context(branches.clone());
|
||||
// Suppress startup warning about using <Redirect/> without ServerRedirectFunction:
|
||||
provide_server_redirect(|_str| ());
|
||||
|
||||
additional_context();
|
||||
|
||||
leptos::suppress_resource_load(true);
|
||||
_ = app_fn().into_view();
|
||||
leptos::suppress_resource_load(false);
|
||||
|
||||
branches
|
||||
}
|
|
@ -1,258 +0,0 @@
|
|||
// This is here, vs /router/tests/, because it accesses some `pub(crate)`
|
||||
// features to test crate internals that wouldn't be available there.
|
||||
|
||||
#![cfg(all(test, feature = "ssr"))]
|
||||
|
||||
use crate::*;
|
||||
use itertools::Itertools;
|
||||
use leptos::*;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
#[component]
|
||||
fn DefaultApp() -> impl IntoView {
|
||||
let view = || view! { "" };
|
||||
view! {
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/foo" view/>
|
||||
<Route path="/bar/" view/>
|
||||
<Route path="/baz/:id" view/>
|
||||
<Route path="/name/:name/" view/>
|
||||
<Route path="/any/*any" view/>
|
||||
</Routes>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ExactApp() -> impl IntoView {
|
||||
let view = || view! { "" };
|
||||
let trailing_slash = TrailingSlash::Exact;
|
||||
view! {
|
||||
<Router trailing_slash>
|
||||
<Routes>
|
||||
<Route path="/foo" view/>
|
||||
<Route path="/bar/" view/>
|
||||
<Route path="/baz/:id" view/>
|
||||
<Route path="/name/:name/" view/>
|
||||
<Route path="/any/*any" view/>
|
||||
</Routes>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn RedirectApp() -> impl IntoView {
|
||||
let view = || view! { "" };
|
||||
let trailing_slash = TrailingSlash::Redirect;
|
||||
view! {
|
||||
<Router trailing_slash>
|
||||
<Routes>
|
||||
<Route path="/foo" view/>
|
||||
<Route path="/bar/" view/>
|
||||
<Route path="/baz/:id" view/>
|
||||
<Route path="/name/:name/" view/>
|
||||
<Route path="/any/*any" view/>
|
||||
</Routes>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_generated_routes_default() {
|
||||
// By default, we use the behavior as of Leptos 0.5, which is equivalent to TrailingSlash::Drop.
|
||||
assert_generated_paths(
|
||||
DefaultApp,
|
||||
&["/any/*any", "/bar", "/baz/:id", "/foo", "/name/:name"],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generated_routes_exact() {
|
||||
// Allow users to precisely define whether slashes are present:
|
||||
assert_generated_paths(
|
||||
ExactApp,
|
||||
&["/any/*any", "/bar/", "/baz/:id", "/foo", "/name/:name/"],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generated_routes_redirect() {
|
||||
// TralingSlashes::Redirect generates paths to redirect to the path with the "correct" trailing slash ending (or lack thereof).
|
||||
assert_generated_paths(
|
||||
RedirectApp,
|
||||
&[
|
||||
"/any/*any",
|
||||
"/bar",
|
||||
"/bar/",
|
||||
"/baz/:id",
|
||||
"/baz/:id/",
|
||||
"/foo",
|
||||
"/foo/",
|
||||
"/name/:name",
|
||||
"/name/:name/",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rendered_redirect() {
|
||||
// Given an app that uses TrailngSlsahes::Redirect, rendering the redirected path
|
||||
// should render the redirect. Other paths should not.
|
||||
|
||||
let expected_redirects = &[
|
||||
("/bar", "/bar/"),
|
||||
("/baz/some_id/", "/baz/some_id"),
|
||||
("/name/some_name", "/name/some_name/"),
|
||||
("/foo/", "/foo"),
|
||||
];
|
||||
|
||||
let redirect_result = Rc::new(RefCell::new(Option::None));
|
||||
let rc = redirect_result.clone();
|
||||
let server_redirect = move |new_value: &str| {
|
||||
rc.replace(Some(new_value.to_string()));
|
||||
};
|
||||
|
||||
let _runtime = Disposable(create_runtime());
|
||||
let history = TestHistory::new("/");
|
||||
provide_context(RouterIntegrationContext::new(history.clone()));
|
||||
provide_server_redirect(server_redirect);
|
||||
|
||||
// We expect these redirects to exist:
|
||||
for (src, dest) in expected_redirects {
|
||||
let loc = format!("https://example.com{src}");
|
||||
history.goto(&loc);
|
||||
redirect_result.replace(None);
|
||||
RedirectApp().into_view().render_to_string();
|
||||
let redirected_to = redirect_result.borrow().clone();
|
||||
assert!(
|
||||
redirected_to.is_some(),
|
||||
"Should redirect from {src} to {dest}"
|
||||
);
|
||||
assert_eq!(redirected_to.unwrap(), *dest);
|
||||
}
|
||||
|
||||
// But the destination paths shouldn't themselves redirect:
|
||||
redirect_result.replace(None);
|
||||
for (_src, dest) in expected_redirects {
|
||||
let loc = format!("https://example.com{dest}");
|
||||
history.goto(&loc);
|
||||
RedirectApp().into_view().render_to_string();
|
||||
let redirected_to = redirect_result.borrow().clone();
|
||||
assert!(
|
||||
redirected_to.is_none(),
|
||||
"Destination of redirect shouldn't also redirect: {dest}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
struct Disposable(RuntimeId);
|
||||
|
||||
// If the test fails, and we don't dispose, we get irrelevant panics.
|
||||
impl Drop for Disposable {
|
||||
fn drop(&mut self) {
|
||||
self.0.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TestHistory {
|
||||
loc: RwSignal<LocationChange>,
|
||||
}
|
||||
|
||||
impl TestHistory {
|
||||
fn new(initial: &str) -> Self {
|
||||
let lc = LocationChange {
|
||||
value: initial.to_owned(),
|
||||
..Default::default()
|
||||
};
|
||||
Self {
|
||||
loc: create_rw_signal(lc),
|
||||
}
|
||||
}
|
||||
|
||||
fn goto(&self, loc: &str) {
|
||||
let change = LocationChange {
|
||||
value: loc.to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
self.navigate(&change);
|
||||
}
|
||||
}
|
||||
|
||||
impl History for TestHistory {
|
||||
fn location(&self) -> ReadSignal<LocationChange> {
|
||||
self.loc.read_only()
|
||||
}
|
||||
|
||||
fn navigate(&self, new_loc: &LocationChange) {
|
||||
self.loc.update(|loc| loc.value.clone_from(&new_loc.value))
|
||||
}
|
||||
}
|
||||
|
||||
// WARNING!
|
||||
//
|
||||
// Despite generate_route_list_inner() using a new leptos_reactive::RuntimeID
|
||||
// each time we call this function, somehow Routes are leaked between different
|
||||
// apps. To avoid that, make sure to put each call in a separate #[test] method.
|
||||
//
|
||||
// TODO: Better isolation for different apps to avoid this issue?
|
||||
fn assert_generated_paths<F, IV>(app: F, expected_sorted_paths: &[&str])
|
||||
where
|
||||
F: Clone + Fn() -> IV + 'static,
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let (routes, static_data) = generate_route_list_inner(app);
|
||||
|
||||
let mut paths = routes.iter().map(|route| route.path()).collect_vec();
|
||||
paths.sort();
|
||||
|
||||
assert_eq!(paths, expected_sorted_paths);
|
||||
|
||||
let mut keys = static_data.keys().collect_vec();
|
||||
keys.sort();
|
||||
assert_eq!(paths, keys);
|
||||
|
||||
// integrations can update "path" to be valid for themselves, but
|
||||
// when routes are returned by leptos_router, these are equal:
|
||||
assert!(routes
|
||||
.iter()
|
||||
.all(|route| route.path() == route.leptos_path()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unique_route_ids() {
|
||||
let branches = get_branches(RedirectApp);
|
||||
assert!(!branches.is_empty());
|
||||
|
||||
assert!(branches
|
||||
.iter()
|
||||
.flat_map(|branch| &branch.routes)
|
||||
.map(|route| route.id)
|
||||
.all_unique());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unique_route_patterns() {
|
||||
let branches = get_branches(RedirectApp);
|
||||
assert!(!branches.is_empty());
|
||||
|
||||
assert!(branches
|
||||
.iter()
|
||||
.flat_map(|branch| &branch.routes)
|
||||
.map(|route| route.pattern.as_str())
|
||||
.all_unique());
|
||||
}
|
||||
|
||||
fn get_branches<F, IV>(app_fn: F) -> Vec<Branch>
|
||||
where
|
||||
F: Fn() -> IV + Clone + 'static,
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let runtime = create_runtime();
|
||||
let additional_context = || ();
|
||||
let branches = super::get_branches(app_fn, additional_context);
|
||||
let branches = branches.0.borrow().clone();
|
||||
runtime.dispose();
|
||||
branches
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
use super::params::ParamsMap;
|
||||
use crate::{State, Url};
|
||||
use leptos::*;
|
||||
|
||||
/// Creates a reactive location from the given path and state.
|
||||
pub fn create_location(
|
||||
path: ReadSignal<String>,
|
||||
state: ReadSignal<State>,
|
||||
) -> Location {
|
||||
let url = create_memo(move |prev: Option<&Url>| {
|
||||
path.with(|path| match Url::try_from(path.as_str()) {
|
||||
Ok(url) => url,
|
||||
Err(e) => {
|
||||
leptos::logging::error!(
|
||||
"[Leptos Router] Invalid path {path}\n\n{e:?}"
|
||||
);
|
||||
prev.cloned().unwrap()
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let pathname = create_memo(move |_| url.with(|url| url.pathname.clone()));
|
||||
let search = create_memo(move |_| url.with(|url| url.search.clone()));
|
||||
let hash = create_memo(move |_| url.with(|url| url.hash.clone()));
|
||||
let query = create_memo(move |_| url.with(|url| url.search_params.clone()));
|
||||
|
||||
Location {
|
||||
pathname,
|
||||
search,
|
||||
hash,
|
||||
query,
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
/// 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,
|
||||
}
|
||||
|
||||
impl Default for LocationChange {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
value: Default::default(),
|
||||
replace: true,
|
||||
scroll: true,
|
||||
state: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,221 +0,0 @@
|
|||
use leptos::*;
|
||||
use std::rc::Rc;
|
||||
use wasm_bindgen::UnwrapThrowExt;
|
||||
|
||||
mod location;
|
||||
mod params;
|
||||
mod state;
|
||||
mod url;
|
||||
|
||||
pub use self::url::*;
|
||||
pub use location::*;
|
||||
pub use params::*;
|
||||
pub use state::*;
|
||||
|
||||
impl core::fmt::Debug for RouterIntegrationContext {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("RouterIntegrationContext").finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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) -> 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 {}
|
||||
|
||||
impl BrowserIntegration {
|
||||
fn current() -> LocationChange {
|
||||
let loc = leptos_dom::helpers::location();
|
||||
let state = window()
|
||||
.history()
|
||||
.and_then(|h| h.state())
|
||||
.ok()
|
||||
.and_then(|s| (!s.is_null()).then_some(s));
|
||||
|
||||
LocationChange {
|
||||
value: loc.pathname().unwrap_or_default()
|
||||
+ loc.search().unwrap_or_default().as_str()
|
||||
+ loc.hash().unwrap_or_default().as_str(),
|
||||
replace: true,
|
||||
scroll: true,
|
||||
state: State(state),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl History for BrowserIntegration {
|
||||
fn location(&self) -> ReadSignal<LocationChange> {
|
||||
use crate::{NavigateOptions, RouterContext};
|
||||
|
||||
let (location, set_location) = create_signal(Self::current());
|
||||
|
||||
leptos::window_event_listener_untyped("popstate", move |_| {
|
||||
let router = use_context::<RouterContext>();
|
||||
if let Some(router) = router {
|
||||
let path_stack = router.inner.path_stack;
|
||||
|
||||
let is_back = router.inner.is_back;
|
||||
let change = Self::current();
|
||||
|
||||
let is_navigating_back = path_stack.with_value(|stack| {
|
||||
stack.len() == 1
|
||||
|| (stack.len() >= 2
|
||||
&& stack.get(stack.len() - 2)
|
||||
== Some(&change.value))
|
||||
});
|
||||
if is_navigating_back {
|
||||
path_stack.update_value(|stack| {
|
||||
stack.pop();
|
||||
});
|
||||
}
|
||||
|
||||
is_back.set(is_navigating_back);
|
||||
|
||||
request_animation_frame(move || {
|
||||
is_back.set(false);
|
||||
});
|
||||
if let Err(e) = router.inner.navigate_from_route(
|
||||
&change.value,
|
||||
&NavigateOptions {
|
||||
resolve: false,
|
||||
replace: change.replace,
|
||||
scroll: change.scroll,
|
||||
state: change.state,
|
||||
},
|
||||
) {
|
||||
leptos::logging::error!("{e:#?}");
|
||||
}
|
||||
set_location.set(Self::current());
|
||||
} else {
|
||||
leptos::logging::warn!("RouterContext not found");
|
||||
}
|
||||
});
|
||||
|
||||
location
|
||||
}
|
||||
|
||||
fn navigate(&self, loc: &LocationChange) {
|
||||
let history = leptos_dom::window().history().unwrap_throw();
|
||||
|
||||
if loc.replace {
|
||||
history
|
||||
.replace_state_with_url(
|
||||
&loc.state.to_js_value(),
|
||||
"",
|
||||
Some(&loc.value),
|
||||
)
|
||||
.unwrap_throw();
|
||||
} else {
|
||||
// push the "forward direction" marker
|
||||
let state = &loc.state.to_js_value();
|
||||
history
|
||||
.push_state_with_url(state, "", Some(&loc.value))
|
||||
.unwrap_throw();
|
||||
}
|
||||
// scroll to el
|
||||
scroll_to_el(loc.scroll);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn scroll_to_el(loc_scroll: bool) {
|
||||
if let Ok(hash) = leptos_dom::helpers::location().hash() {
|
||||
if !hash.is_empty() {
|
||||
let hash = js_sys::decode_uri(&hash[1..])
|
||||
.ok()
|
||||
.and_then(|decoded| decoded.as_string())
|
||||
.unwrap_or(hash);
|
||||
let el = leptos_dom::document().get_element_by_id(&hash);
|
||||
if let Some(el) = el {
|
||||
el.scroll_into_view();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// scroll to top
|
||||
if loc_scroll {
|
||||
leptos_dom::window().scroll_to_with_x_and_y(0.0, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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. Be sure that it can survive conversion to a URL in the browser.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos_router::*;
|
||||
/// # use leptos::*;
|
||||
/// # let rt = create_runtime();
|
||||
/// let integration = ServerIntegration {
|
||||
/// path: "http://leptos.rs/".to_string(),
|
||||
/// };
|
||||
/// provide_context(RouterIntegrationContext::new(integration));
|
||||
/// # rt.dispose();
|
||||
/// ```
|
||||
#[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))
|
||||
}
|
||||
}
|
||||
|
||||
impl History for RouterIntegrationContext {
|
||||
fn location(&self) -> ReadSignal<LocationChange> {
|
||||
self.0.location()
|
||||
}
|
||||
|
||||
fn navigate(&self, loc: &LocationChange) {
|
||||
self.0.navigate(loc)
|
||||
}
|
||||
}
|
||||
|
||||
/// A generic router integration for the server side.
|
||||
///
|
||||
/// This should match what the browser history will show.
|
||||
///
|
||||
/// Generally, this will already be provided if you are using the leptos
|
||||
/// server integrations.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos_router::*;
|
||||
/// # use leptos::*;
|
||||
/// # let rt = create_runtime();
|
||||
/// let integration = ServerIntegration {
|
||||
/// // Swap out with your URL if integrating manually.
|
||||
/// path: "http://leptos.rs/".to_string(),
|
||||
/// };
|
||||
/// provide_context(RouterIntegrationContext::new(integration));
|
||||
/// # rt.dispose();
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ServerIntegration {
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
impl History for ServerIntegration {
|
||||
fn location(&self) -> ReadSignal<LocationChange> {
|
||||
create_signal(LocationChange {
|
||||
value: self.path.clone(),
|
||||
replace: false,
|
||||
scroll: true,
|
||||
state: State(None),
|
||||
})
|
||||
.0
|
||||
}
|
||||
|
||||
fn navigate(&self, _loc: &LocationChange) {}
|
||||
}
|
|
@ -1,196 +0,0 @@
|
|||
use linear_map::LinearMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
use thiserror::Error;
|
||||
|
||||
/// 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`
|
||||
///
|
||||
/// [`LinearMap`]: linear_map::LinearMap
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
#[repr(transparent)]
|
||||
pub struct ParamsMap(pub LinearMap<String, String>);
|
||||
|
||||
impl ParamsMap {
|
||||
/// Creates an empty map.
|
||||
#[inline(always)]
|
||||
pub fn new() -> Self {
|
||||
Self(LinearMap::new())
|
||||
}
|
||||
|
||||
/// Creates an empty map with the given capacity.
|
||||
#[inline(always)]
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
Self(LinearMap::with_capacity(capacity))
|
||||
}
|
||||
|
||||
/// Inserts a value into the map.
|
||||
#[inline(always)]
|
||||
pub fn insert(&mut self, key: String, value: String) -> Option<String> {
|
||||
self.0.insert(key, value)
|
||||
}
|
||||
|
||||
/// Gets a value from the map.
|
||||
#[inline(always)]
|
||||
pub fn get(&self, key: &str) -> Option<&String> {
|
||||
self.0.get(key)
|
||||
}
|
||||
|
||||
/// Removes a value from the map.
|
||||
#[inline(always)]
|
||||
pub fn remove(&mut self, key: &str) -> Option<String> {
|
||||
self.0.remove(key)
|
||||
}
|
||||
|
||||
/// Converts the map to a query string.
|
||||
pub fn to_query_string(&self) -> String {
|
||||
use crate::history::url::escape;
|
||||
let mut buf = String::new();
|
||||
if !self.0.is_empty() {
|
||||
buf.push('?');
|
||||
for (k, v) in &self.0 {
|
||||
buf.push_str(&escape(k));
|
||||
buf.push('=');
|
||||
buf.push_str(&escape(v));
|
||||
buf.push('&');
|
||||
}
|
||||
if buf.len() > 1 {
|
||||
buf.pop();
|
||||
}
|
||||
}
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ParamsMap {
|
||||
#[inline(always)]
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [`ParamsMap`] in a declarative style.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos_router::params_map;
|
||||
/// # #[cfg(feature = "ssr")] {
|
||||
/// let map = params_map! {
|
||||
/// "crate" => "leptos",
|
||||
/// 42 => true, // where key & val: core::fmt::Display
|
||||
/// };
|
||||
/// assert_eq!(map.get("crate"), Some(&"leptos".to_string()));
|
||||
/// assert_eq!(map.get("42"), Some(&true.to_string()))
|
||||
/// # }
|
||||
/// ```
|
||||
// Original implementation included the below credits.
|
||||
//
|
||||
// Adapted from hash_map! in common_macros crate
|
||||
// Copyright (c) 2019 Philipp Korber
|
||||
// https://github.com/rustonaut/common_macros/blob/master/src/lib.rs
|
||||
#[macro_export]
|
||||
macro_rules! params_map {
|
||||
// Fast path avoids allocation.
|
||||
() => { $crate::ParamsMap::with_capacity(0) };
|
||||
|
||||
// Counting repitions by n = 0 ( + 1 )*
|
||||
//
|
||||
// https://github.com/rust-lang/rust/issues/83527
|
||||
// When stabilized you can use "metavaribale exprs" instead
|
||||
//
|
||||
// `$key | $val` must be included in the repetition to be valid, it is
|
||||
// stringified to null out any possible side-effects.
|
||||
($($key:expr => $val:expr),* $(,)?) => {{
|
||||
let n = 0 $(+ { _ = stringify!($key); 1 })*;
|
||||
#[allow(unused_mut)]
|
||||
let mut map = $crate::ParamsMap::with_capacity(n);
|
||||
$( map.insert($key.to_string(), $val.to_string()); )*
|
||||
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,
|
||||
{
|
||||
/// Attempts to deserialize the map into the given type.
|
||||
fn from_map(map: &ParamsMap) -> Result<Self, ParamsError>;
|
||||
}
|
||||
|
||||
impl Params for () {
|
||||
#[inline(always)]
|
||||
fn from_map(_map: &ParamsMap) -> Result<Self, ParamsError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoParam
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
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,
|
||||
{
|
||||
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) => Err(ParamsError::Params(Arc::new(e))),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "nightly")] {
|
||||
auto trait NotOption {}
|
||||
impl<T> !NotOption for Option<T> {}
|
||||
|
||||
impl<T> IntoParam for T
|
||||
where
|
||||
T: FromStr + NotOption,
|
||||
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
fn into_param(value: Option<&str>, name: &str) -> Result<Self, ParamsError> {
|
||||
let value = value.ok_or_else(|| ParamsError::MissingParam(name.to_string()))?;
|
||||
Self::from_str(value).map_err(|e| ParamsError::Params(Arc::new(e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors that can occur while parsing params using [`Params`].
|
||||
#[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(Arc<dyn std::error::Error + Send + Sync>),
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
use wasm_bindgen::JsValue;
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct State(pub Option<JsValue>);
|
||||
|
||||
impl State {
|
||||
pub fn to_js_value(&self) -> JsValue {
|
||||
match &self.0 {
|
||||
Some(v) => v.clone(),
|
||||
None => JsValue::NULL,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for State
|
||||
where
|
||||
T: Into<JsValue>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
State(Some(value.into()))
|
||||
}
|
||||
}
|
|
@ -1,134 +0,0 @@
|
|||
use crate::ParamsMap;
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
use js_sys::{try_iter, Array, JsString};
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
use wasm_bindgen::JsCast;
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct Url {
|
||||
pub origin: String,
|
||||
pub pathname: String,
|
||||
pub search: String,
|
||||
pub search_params: ParamsMap,
|
||||
pub hash: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn unescape(s: &str) -> String {
|
||||
percent_encoding::percent_decode_str(s)
|
||||
.decode_utf8()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn unescape(s: &str) -> String {
|
||||
js_sys::decode_uri_component(s).unwrap().into()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn unescape_minimal(s: &str) -> String {
|
||||
js_sys::decode_uri(s).unwrap().into()
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn escape(s: &str) -> String {
|
||||
percent_encoding::utf8_percent_encode(s, percent_encoding::NON_ALPHANUMERIC)
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn escape(s: &str) -> String {
|
||||
js_sys::encode_uri_component(s).as_string().unwrap()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
impl TryFrom<&str> for Url {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(url: &str) -> Result<Self, Self::Error> {
|
||||
let url = web_sys::Url::new_with_base(
|
||||
&if url.starts_with("//") {
|
||||
let origin =
|
||||
leptos::window().location().origin().unwrap_or_default();
|
||||
format!("{origin}{url}")
|
||||
} else {
|
||||
url.to_string()
|
||||
},
|
||||
"http://leptos",
|
||||
)
|
||||
.map_js_error()?;
|
||||
Ok(Self {
|
||||
origin: url.origin(),
|
||||
pathname: url.pathname(),
|
||||
search: url
|
||||
.search()
|
||||
.strip_prefix('?')
|
||||
.map(String::from)
|
||||
.unwrap_or_default(),
|
||||
search_params: ParamsMap(
|
||||
try_iter(&url.search_params())
|
||||
.map_js_error()?
|
||||
.ok_or(
|
||||
"Failed to use URLSearchParams as an iterator"
|
||||
.to_string(),
|
||||
)?
|
||||
.map(|value| {
|
||||
let array: Array =
|
||||
value.map_js_error()?.dyn_into().map_js_error()?;
|
||||
Ok((
|
||||
array
|
||||
.get(0)
|
||||
.dyn_into::<JsString>()
|
||||
.map_js_error()?
|
||||
.into(),
|
||||
array
|
||||
.get(1)
|
||||
.dyn_into::<JsString>()
|
||||
.map_js_error()?
|
||||
.into(),
|
||||
))
|
||||
})
|
||||
.collect::<Result<
|
||||
linear_map::LinearMap<String, String>,
|
||||
Self::Error,
|
||||
>>()?,
|
||||
),
|
||||
hash: url.hash(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
impl TryFrom<&str> for Url {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(url: &str) -> Result<Self, Self::Error> {
|
||||
let url = url::Url::parse(url).map_err(|e| e.to_string())?;
|
||||
Ok(Self {
|
||||
origin: url.origin().unicode_serialization(),
|
||||
pathname: url.path().to_string(),
|
||||
search: url.query().unwrap_or_default().to_string(),
|
||||
search_params: ParamsMap(
|
||||
url.query_pairs()
|
||||
.map(|(key, value)| (key.to_string(), value.to_string()))
|
||||
.collect::<linear_map::LinearMap<String, String>>(),
|
||||
),
|
||||
hash: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
trait MapJsError<T> {
|
||||
fn map_js_error(self) -> Result<T, String>;
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
impl<T> MapJsError<T> for Result<T, JsValue> {
|
||||
fn map_js_error(self) -> Result<T, String> {
|
||||
self.map_err(|e| e.as_string().unwrap_or_default())
|
||||
}
|
||||
}
|
|
@ -1,242 +0,0 @@
|
|||
use crate::{
|
||||
Location, NavigateOptions, Params, ParamsError, ParamsMap, RouteContext,
|
||||
RouterContext,
|
||||
};
|
||||
use leptos::{
|
||||
request_animation_frame, signal_prelude::*, use_context, window, Oco,
|
||||
};
|
||||
use std::{rc::Rc, str::FromStr};
|
||||
|
||||
/// Constructs a signal synchronized with a specific URL query parameter.
|
||||
///
|
||||
/// The function creates a bidirectional sync mechanism between the state encapsulated in a signal and a URL query parameter.
|
||||
/// This means that any change to the state will update the URL, and vice versa, making the function especially useful
|
||||
/// for maintaining state consistency across page reloads.
|
||||
///
|
||||
/// The `key` argument is the unique identifier for the query parameter to be synced with the state.
|
||||
/// It is important to note that only one state can be tied to a specific key at any given time.
|
||||
///
|
||||
/// The function operates with types that can be parsed from and formatted into strings, denoted by `T`.
|
||||
/// If the parsing fails for any reason, the function treats the value as `None`.
|
||||
/// The URL parameter can be cleared by setting the signal to `None`.
|
||||
///
|
||||
/// ```rust
|
||||
/// use leptos::*;
|
||||
/// use leptos_router::*;
|
||||
///
|
||||
/// #[component]
|
||||
/// pub fn SimpleQueryCounter() -> impl IntoView {
|
||||
/// let (count, set_count) = create_query_signal::<i32>("count");
|
||||
/// let clear = move |_| set_count.set(None);
|
||||
/// let decrement =
|
||||
/// move |_| set_count.set(Some(count.get().unwrap_or(0) - 1));
|
||||
/// let increment =
|
||||
/// move |_| set_count.set(Some(count.get().unwrap_or(0) + 1));
|
||||
///
|
||||
/// view! {
|
||||
/// <div>
|
||||
/// <button on:click=clear>"Clear"</button>
|
||||
/// <button on:click=decrement>"-1"</button>
|
||||
/// <span>"Value: " {move || count.get().unwrap_or(0)} "!"</span>
|
||||
/// <button on:click=increment>"+1"</button>
|
||||
/// </div>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[track_caller]
|
||||
pub fn create_query_signal<T>(
|
||||
key: impl Into<Oco<'static, str>>,
|
||||
) -> (Memo<Option<T>>, SignalSetter<Option<T>>)
|
||||
where
|
||||
T: FromStr + ToString + PartialEq,
|
||||
{
|
||||
let mut key: Oco<'static, str> = key.into();
|
||||
let query_map = use_query_map();
|
||||
let navigate = use_navigate();
|
||||
let location = use_location();
|
||||
|
||||
let get = create_memo({
|
||||
let key = key.clone_inplace();
|
||||
move |_| {
|
||||
query_map
|
||||
.with(|map| map.get(&key).and_then(|value| value.parse().ok()))
|
||||
}
|
||||
});
|
||||
|
||||
let set = SignalSetter::map(move |value: Option<T>| {
|
||||
let mut new_query_map = query_map.get();
|
||||
match value {
|
||||
Some(value) => {
|
||||
new_query_map.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
None => {
|
||||
new_query_map.remove(&key);
|
||||
}
|
||||
}
|
||||
let qs = new_query_map.to_query_string();
|
||||
let path = location.pathname.get_untracked();
|
||||
let hash = location.hash.get_untracked();
|
||||
let new_url = format!("{path}{qs}{hash}");
|
||||
navigate(&new_url, NavigateOptions::default());
|
||||
});
|
||||
|
||||
(get, set)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn has_router() -> bool {
|
||||
use_context::<RouterContext>().is_some()
|
||||
}
|
||||
|
||||
/// Returns the current [`RouterContext`], containing information about the router's state.
|
||||
#[track_caller]
|
||||
pub fn use_router() -> RouterContext {
|
||||
if let Some(router) = use_context::<RouterContext>() {
|
||||
router
|
||||
} else {
|
||||
leptos::leptos_dom::debug_warn!(
|
||||
"You must call use_router() within a <Router/> component {:?}",
|
||||
std::panic::Location::caller()
|
||||
);
|
||||
panic!("You must call use_router() within a <Router/> component");
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current [`RouteContext`], containing information about the matched route.
|
||||
#[track_caller]
|
||||
pub fn use_route() -> RouteContext {
|
||||
use_context::<RouteContext>().unwrap_or_else(|| use_router().base())
|
||||
}
|
||||
|
||||
/// Returns the data for the current route, which is provided by the `data` prop on `<Route/>`.
|
||||
#[track_caller]
|
||||
pub fn use_route_data<T: Clone + 'static>() -> Option<T> {
|
||||
let route = use_context::<RouteContext>()?;
|
||||
let data = route.inner.data.borrow();
|
||||
let data = data.clone()?;
|
||||
let downcast = data.downcast_ref::<T>().cloned();
|
||||
downcast
|
||||
}
|
||||
|
||||
/// Returns the current [`Location`], which contains reactive variables
|
||||
#[track_caller]
|
||||
pub fn use_location() -> Location {
|
||||
use_router().inner.location.clone()
|
||||
}
|
||||
|
||||
/// Returns a raw key-value map of route params.
|
||||
#[track_caller]
|
||||
pub fn use_params_map() -> Memo<ParamsMap> {
|
||||
let route = use_route();
|
||||
route.params()
|
||||
}
|
||||
|
||||
/// Returns the current route params, parsed into the given type, or an error.
|
||||
#[track_caller]
|
||||
pub fn use_params<T>() -> Memo<Result<T, ParamsError>>
|
||||
where
|
||||
T: Params + PartialEq,
|
||||
{
|
||||
let route = use_route();
|
||||
create_memo(move |_| route.params().with(T::from_map))
|
||||
}
|
||||
|
||||
/// Returns a raw key-value map of the URL search query.
|
||||
#[track_caller]
|
||||
pub fn use_query_map() -> Memo<ParamsMap> {
|
||||
use_router().inner.location.query
|
||||
}
|
||||
|
||||
/// Returns the current URL search query, parsed into the given type, or an error.
|
||||
#[track_caller]
|
||||
pub fn use_query<T>() -> Memo<Result<T, ParamsError>>
|
||||
where
|
||||
T: Params + PartialEq,
|
||||
{
|
||||
let router = use_router();
|
||||
create_memo(move |_| router.inner.location.query.with(|m| T::from_map(m)))
|
||||
}
|
||||
|
||||
/// Resolves the given path relative to the current route.
|
||||
#[track_caller]
|
||||
pub fn use_resolved_path(
|
||||
path: impl Fn() -> String + 'static,
|
||||
) -> Memo<Option<String>> {
|
||||
let route = use_route();
|
||||
|
||||
create_memo(move |_| {
|
||||
let path = path();
|
||||
if path.starts_with('/') {
|
||||
Some(path)
|
||||
} else {
|
||||
route.resolve_path_tracked(&path)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a function that can be used to navigate to a new route.
|
||||
///
|
||||
/// This should only be called on the client; it does nothing during
|
||||
/// server rendering.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use leptos::{request_animation_frame, create_runtime};
|
||||
/// # let runtime = create_runtime();
|
||||
/// # if false { // can't actually navigate, no <Router/>
|
||||
/// let navigate = leptos_router::use_navigate();
|
||||
/// navigate("/", Default::default());
|
||||
/// # }
|
||||
/// # runtime.dispose();
|
||||
/// ```
|
||||
#[track_caller]
|
||||
pub fn use_navigate() -> impl Fn(&str, NavigateOptions) + Clone {
|
||||
let router = use_router();
|
||||
move |to, options| {
|
||||
let router = Rc::clone(&router.inner);
|
||||
let to = to.to_string();
|
||||
if cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
request_animation_frame(move || {
|
||||
#[allow(unused_variables)]
|
||||
if let Err(e) = router.navigate_from_route(&to, &options) {
|
||||
leptos::logging::debug_warn!("use_navigate error: {e:?}");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
leptos::logging::warn!(
|
||||
"The navigation function returned by `use_navigate` should \
|
||||
not be called during server rendering."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a signal that tells you whether you are currently navigating backwards.
|
||||
pub(crate) fn use_is_back_navigation() -> ReadSignal<bool> {
|
||||
let router = use_router();
|
||||
router.inner.is_back.read_only()
|
||||
}
|
||||
|
||||
/// Resolves a redirect location to an (absolute) URL.
|
||||
pub(crate) fn resolve_redirect_url(loc: &str) -> Option<web_sys::Url> {
|
||||
let origin = match window().location().origin() {
|
||||
Ok(origin) => origin,
|
||||
Err(e) => {
|
||||
leptos::logging::error!("Failed to get origin: {:#?}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Use server function's URL as base instead.
|
||||
let base = origin;
|
||||
|
||||
match web_sys::Url::new_with_base(loc, &base) {
|
||||
Ok(url) => Some(url),
|
||||
Err(e) => {
|
||||
leptos::logging::error!(
|
||||
"Invalid redirect location: {}",
|
||||
e.as_string().unwrap_or_default(),
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,208 +0,0 @@
|
|||
#![forbid(unsafe_code)]
|
||||
|
||||
//! # Leptos Router
|
||||
//!
|
||||
//! Leptos Router is a router and state management tool for web applications
|
||||
//! written in Rust using the [`Leptos`] web framework.
|
||||
//! It is ”isomorphic”, i.e., it can be used for client-side applications/single-page
|
||||
//! apps (SPAs), server-side rendering/multi-page apps (MPAs), or to synchronize
|
||||
//! state between the two.
|
||||
//!
|
||||
//! ## Philosophy
|
||||
//!
|
||||
//! Leptos Router is built on a few simple principles:
|
||||
//! 1. **URL drives state.** For web applications, the URL should be the ultimate
|
||||
//! source of truth for most of your app’s state. (It’s called a **Universal
|
||||
//! Resource Locator** for a reason!)
|
||||
//!
|
||||
//! 2. **Nested routing.** A URL can match multiple routes that exist in a nested tree
|
||||
//! and are rendered by different components. This means you can navigate between siblings
|
||||
//! in this tree without re-rendering or triggering any change in the parent routes.
|
||||
//!
|
||||
//! 3. **Progressive enhancement.** The [`A`] and [`Form`] components resolve any relative
|
||||
//! nested routes, render actual `<a>` and `<form>` elements, and (when possible)
|
||||
//! upgrading them to handle those navigations with client-side routing. If you’re using
|
||||
//! them with server-side rendering (with or without hydration), they just work,
|
||||
//! whether JS/WASM have loaded or not.
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! use leptos::*;
|
||||
//! use leptos_router::*;
|
||||
//!
|
||||
//! #[component]
|
||||
//! pub fn RouterExample() -> impl IntoView {
|
||||
//! view! {
|
||||
//!
|
||||
//! <div id="root">
|
||||
//! // we wrap the whole app in a <Router/> to allow client-side navigation
|
||||
//! // from our nav links below
|
||||
//! <Router>
|
||||
//! // <nav> and <main> will show on every route
|
||||
//! <nav>
|
||||
//! // LR will enhance the active <a> link with the [aria-current] attribute
|
||||
//! // we can use this for styling them with CSS like `[aria-current] { font-weight: bold; }`
|
||||
//! <A href="contacts">"Contacts"</A>
|
||||
//! // But we can also use a normal class attribute like it is a normal component
|
||||
//! <A href="settings" class="my-class">"Settings"</A>
|
||||
//! // It also supports signals!
|
||||
//! <A href="about" class=move || "my-class">"About"</A>
|
||||
//! </nav>
|
||||
//! <main>
|
||||
//! // <Routes/> both defines our routes and shows them on the page
|
||||
//! <Routes>
|
||||
//! // our root route: the contact list is always shown
|
||||
//! <Route
|
||||
//! path=""
|
||||
//! view=ContactList
|
||||
//! >
|
||||
//! // users like /gbj or /bob
|
||||
//! <Route
|
||||
//! path=":id"
|
||||
//! view=Contact
|
||||
//! />
|
||||
//! // a fallback if the /:id segment is missing from the URL
|
||||
//! <Route
|
||||
//! path=""
|
||||
//! view=move || view! { <p class="contact">"Select a contact."</p> }
|
||||
//! />
|
||||
//! </Route>
|
||||
//! // LR will automatically use this for /about, not the /:id match above
|
||||
//! <Route
|
||||
//! path="about"
|
||||
//! view=About
|
||||
//! />
|
||||
//! </Routes>
|
||||
//! </main>
|
||||
//! </Router>
|
||||
//! </div>
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! type ContactSummary = (); // TODO!
|
||||
//! type Contact = (); // TODO!()
|
||||
//!
|
||||
//! // contact_data reruns whenever the :id param changes
|
||||
//! 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() -> Vec<ContactSummary> {
|
||||
//! todo!()
|
||||
//! }
|
||||
//!
|
||||
//! #[component]
|
||||
//! fn ContactList() -> impl IntoView {
|
||||
//! // loads the contact list data once; doesn't reload when nested routes change
|
||||
//! let contacts = create_resource(|| (), |_| contact_list_data());
|
||||
//! view! {
|
||||
//!
|
||||
//! <div>
|
||||
//! // show the contacts
|
||||
//! <ul>
|
||||
//! {move || contacts.read().map(|contacts| view! { <li>"todo contact info"</li> } )}
|
||||
//! </ul>
|
||||
//!
|
||||
//! // insert the nested child route here
|
||||
//! <Outlet/>
|
||||
//! </div>
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! #[component]
|
||||
//! fn Contact() -> impl IntoView {
|
||||
//! let params = use_params_map();
|
||||
//! let data = create_resource(
|
||||
//!
|
||||
//! move || params.with(|p| p.get("id").cloned().unwrap_or_default()),
|
||||
//! move |id| contact_data(id)
|
||||
//! );
|
||||
//! todo!()
|
||||
//! }
|
||||
//!
|
||||
//! #[component]
|
||||
//! fn About() -> impl IntoView {
|
||||
//! todo!()
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Module Route Definitions
|
||||
//! Routes can also be modularized and nested by defining them in separate components, which can be
|
||||
//! located in and imported from other modules. Components that return `<Route/>` should be marked
|
||||
//! `#[component(transparent)]`, as in this example:
|
||||
//! ```rust
|
||||
//! use leptos::*;
|
||||
//! use leptos_router::*;
|
||||
//!
|
||||
//! #[component]
|
||||
//! pub fn App() -> impl IntoView {
|
||||
//! view! {
|
||||
//! <Router>
|
||||
//! <Routes>
|
||||
//! <Route path="/" view=move || {
|
||||
//! view! { "-> /" }
|
||||
//! }/>
|
||||
//! <ExternallyDefinedRoute/>
|
||||
//! </Routes>
|
||||
//! </Router>
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! // `transparent` here marks the component as returning data (a RouteDefinition), not a view
|
||||
//! #[component(transparent)]
|
||||
//! pub fn ExternallyDefinedRoute() -> impl IntoView {
|
||||
//! view! {
|
||||
//! <Route path="/some-area" view=move || {
|
||||
//! view! { <div>
|
||||
//! <h2>"Some Area"</h2>
|
||||
//! <Outlet/>
|
||||
//! </div> }
|
||||
//! }>
|
||||
//! <Route path="/path-a/:id" view=move || {
|
||||
//! view! { <p>"Path A"</p> }
|
||||
//! }/>
|
||||
//! <Route path="/path-b/:id" view=move || {
|
||||
//! view! { <p>"Path B"</p> }
|
||||
//! }/>
|
||||
//! </Route>
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! # Feature Flags
|
||||
//! - `csr` Client-side rendering: Generate DOM nodes in the browser
|
||||
//! - `ssr` Server-side rendering: Generate an HTML string (typically on the server)
|
||||
//! - `hydrate` Hydration: use this to add interactivity to an SSRed Leptos app
|
||||
//! - `nightly`: On `nightly` Rust, enables the function-call syntax for signal getters and setters.
|
||||
//!
|
||||
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
|
||||
//! which mode your app is operating in.
|
||||
//!
|
||||
//! [`Leptos`]: <https://github.com/leptos-rs/leptos>
|
||||
|
||||
#![cfg_attr(feature = "nightly", feature(auto_traits))]
|
||||
#![cfg_attr(feature = "nightly", feature(negative_impls))]
|
||||
#![cfg_attr(feature = "nightly", feature(type_name_of_val))]
|
||||
// to prevent warnings from popping up when a nightly feature is stabilized
|
||||
#![allow(stable_features)]
|
||||
|
||||
mod animation;
|
||||
mod components;
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
mod extract_routes;
|
||||
mod history;
|
||||
mod hooks;
|
||||
#[doc(hidden)]
|
||||
pub mod matching;
|
||||
mod render_mode;
|
||||
pub use components::*;
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
pub use extract_routes::*;
|
||||
pub use history::*;
|
||||
pub use hooks::*;
|
||||
pub use matching::{RouteDefinition, *};
|
||||
pub use render_mode::*;
|
||||
extern crate tracing;
|
|
@ -1,105 +0,0 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn expand_optionals(pattern: &str) -> Vec<Cow<'_, str>> {
|
||||
use js_sys::RegExp;
|
||||
use once_cell::unsync::Lazy;
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
thread_local! {
|
||||
static OPTIONAL_RE: Lazy<RegExp> = Lazy::new(|| {
|
||||
RegExp::new(OPTIONAL, "")
|
||||
});
|
||||
static OPTIONAL_RE_2: Lazy<RegExp> = Lazy::new(|| {
|
||||
RegExp::new(OPTIONAL_2, "")
|
||||
});
|
||||
}
|
||||
|
||||
let captures = OPTIONAL_RE.with(|re| re.exec(pattern));
|
||||
match captures {
|
||||
None => vec![pattern.into()],
|
||||
Some(matched) => {
|
||||
let start: usize =
|
||||
js_sys::Reflect::get(&matched, &JsValue::from_str("index"))
|
||||
.unwrap()
|
||||
.as_f64()
|
||||
.unwrap() as usize;
|
||||
let mut prefix = pattern[0..start].to_string();
|
||||
let mut suffix =
|
||||
&pattern[start + matched.get(1).as_string().unwrap().len()..];
|
||||
let mut prefixes = vec![prefix.clone()];
|
||||
|
||||
prefix += &matched.get(1).as_string().unwrap();
|
||||
prefixes.push(prefix.clone());
|
||||
|
||||
while let Some(matched) =
|
||||
OPTIONAL_RE_2.with(|re| re.exec(suffix.trim_start_matches('?')))
|
||||
{
|
||||
prefix += &matched.get(1).as_string().unwrap();
|
||||
prefixes.push(prefix.clone());
|
||||
suffix = &suffix[matched.get(0).as_string().unwrap().len()..];
|
||||
}
|
||||
|
||||
expand_optionals(suffix).iter().fold(
|
||||
Vec::new(),
|
||||
|mut results, expansion| {
|
||||
results.extend(prefixes.iter().map(|prefix| {
|
||||
Cow::Owned(
|
||||
prefix.clone() + expansion.trim_start_matches('?'),
|
||||
)
|
||||
}));
|
||||
results
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn expand_optionals(pattern: &str) -> Vec<Cow<'_, str>> {
|
||||
use regex::Regex;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref OPTIONAL_RE: Regex = Regex::new(OPTIONAL).expect("could not compile OPTIONAL_RE");
|
||||
pub static ref OPTIONAL_RE_2: Regex = Regex::new(OPTIONAL_2).expect("could not compile OPTIONAL_RE_2");
|
||||
}
|
||||
|
||||
let captures = OPTIONAL_RE.find(pattern);
|
||||
match captures {
|
||||
None => vec![pattern.into()],
|
||||
Some(matched) => {
|
||||
let mut prefix = pattern[0..matched.start()].to_string();
|
||||
let captures = OPTIONAL_RE.captures(pattern).unwrap();
|
||||
let mut suffix = &pattern[matched.start() + captures[1].len()..];
|
||||
let mut prefixes = vec![prefix.clone()];
|
||||
|
||||
prefix += &captures[1];
|
||||
prefixes.push(prefix.clone());
|
||||
|
||||
while let Some(captures) =
|
||||
OPTIONAL_RE_2.captures(suffix.trim_start_matches('?'))
|
||||
{
|
||||
prefix += &captures[1];
|
||||
prefixes.push(prefix.clone());
|
||||
suffix = &suffix[captures[0].len()..];
|
||||
}
|
||||
|
||||
expand_optionals(suffix).iter().fold(
|
||||
Vec::new(),
|
||||
|mut results, expansion| {
|
||||
results.extend(prefixes.iter().map(|prefix| {
|
||||
Cow::Owned(
|
||||
prefix.clone() + expansion.trim_start_matches('?'),
|
||||
)
|
||||
}));
|
||||
results
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const OPTIONAL: &str = r"(/?:[^/]+)\?";
|
||||
const OPTIONAL_2: &str = r"^(/:[^/]+)\?";
|
|
@ -1,121 +0,0 @@
|
|||
// Implementation based on Solid Router
|
||||
// see <https://github.com/solidjs/solid-router/blob/main/src/utils.ts>
|
||||
|
||||
use crate::{unescape, ParamsMap};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[doc(hidden)]
|
||||
pub struct PathMatch {
|
||||
pub path: String,
|
||||
pub params: ParamsMap,
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Matcher {
|
||||
splat: Option<String>,
|
||||
segments: Vec<String>,
|
||||
len: usize,
|
||||
partial: bool,
|
||||
}
|
||||
|
||||
impl Matcher {
|
||||
#[doc(hidden)]
|
||||
pub fn new(path: &str) -> Self {
|
||||
Self::new_with_partial(path, false)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn new_with_partial(path: &str, partial: bool) -> Self {
|
||||
let (pattern, splat) = match path.split_once("/*") {
|
||||
Some((p, s)) => (p, Some(s.to_string())),
|
||||
None => (path, None),
|
||||
};
|
||||
let segments: Vec<String> = get_segments(pattern);
|
||||
let len = segments.len();
|
||||
Self {
|
||||
splat,
|
||||
segments,
|
||||
len,
|
||||
partial,
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn test(&self, location: &str) -> Option<PathMatch> {
|
||||
let loc_segments: Vec<&str> = get_segments(location);
|
||||
|
||||
let loc_len = loc_segments.len();
|
||||
let len_diff: i32 = loc_len as i32 - self.len as i32;
|
||||
|
||||
let trailing_iter = location.chars().rev().take_while(|n| *n == '/');
|
||||
|
||||
// quick path: not a match if
|
||||
// 1) matcher has add'l segments not found in location
|
||||
// 2) location has add'l segments, there's no splat, and partial matches not allowed
|
||||
if loc_len < self.len
|
||||
|| (len_diff > 0 && self.splat.is_none() && !self.partial)
|
||||
|| (self.splat.is_none() && trailing_iter.clone().count() > 1)
|
||||
{
|
||||
None
|
||||
}
|
||||
// otherwise, start building a match
|
||||
else {
|
||||
let mut path = String::new();
|
||||
let mut params = ParamsMap::new();
|
||||
|
||||
for (segment, loc_segment) in
|
||||
self.segments.iter().zip(loc_segments.iter())
|
||||
{
|
||||
if let Some(param_name) = segment.strip_prefix(':') {
|
||||
params.insert(param_name.into(), unescape(loc_segment));
|
||||
} else if segment != loc_segment {
|
||||
// if any segment doesn't match and isn't a param, there's no path match
|
||||
return None;
|
||||
}
|
||||
|
||||
path.push('/');
|
||||
path.push_str(loc_segment);
|
||||
}
|
||||
|
||||
if let Some(splat) = &self.splat {
|
||||
if !splat.is_empty() {
|
||||
let mut value = if len_diff > 0 {
|
||||
loc_segments[self.len..].join("/")
|
||||
} else {
|
||||
"".into()
|
||||
};
|
||||
|
||||
// add trailing slashes to splat
|
||||
let trailing_slashes =
|
||||
trailing_iter.skip(1).collect::<String>();
|
||||
value.push_str(&trailing_slashes);
|
||||
|
||||
params.insert(splat.into(), value);
|
||||
}
|
||||
}
|
||||
|
||||
Some(PathMatch { path, params })
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub(crate) fn is_wildcard(&self) -> bool {
|
||||
self.splat.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_segments<'a, S: From<&'a str>>(pattern: &'a str) -> Vec<S> {
|
||||
// URL root paths ("/" and "") are equivalent and treated as 0-segment paths.
|
||||
// non-root paths with trailing slashes get extra empty segment at the end.
|
||||
// This makes sure that segment matching is trailing-slash sensitive.
|
||||
let mut segments: Vec<S> = pattern
|
||||
.split('/')
|
||||
.filter(|p| !p.is_empty())
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
if !segments.is_empty() && pattern.ends_with('/') {
|
||||
segments.push("".into());
|
||||
}
|
||||
segments
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
mod expand_optionals;
|
||||
mod matcher;
|
||||
mod resolve_path;
|
||||
mod route;
|
||||
|
||||
use crate::{Branches, RouteData};
|
||||
pub use expand_optionals::*;
|
||||
pub use matcher::*;
|
||||
pub use resolve_path::*;
|
||||
pub use route::*;
|
||||
use std::rc::Rc;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct RouteMatch {
|
||||
pub path_match: PathMatch,
|
||||
pub route: RouteData,
|
||||
}
|
||||
|
||||
pub(crate) fn get_route_matches(
|
||||
router_id: usize,
|
||||
base: &str,
|
||||
location: String,
|
||||
) -> Rc<Vec<RouteMatch>> {
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
use lru::LruCache;
|
||||
use std::{cell::RefCell, num::NonZeroUsize};
|
||||
type RouteMatchCache = LruCache<(usize, String), Rc<Vec<RouteMatch>>>;
|
||||
thread_local! {
|
||||
static ROUTE_MATCH_CACHE: RefCell<RouteMatchCache> = RefCell::new(LruCache::new(NonZeroUsize::new(32).unwrap()));
|
||||
}
|
||||
|
||||
ROUTE_MATCH_CACHE.with(|cache| {
|
||||
let mut cache = cache.borrow_mut();
|
||||
Rc::clone(
|
||||
cache.get_or_insert((router_id, location.clone()), || {
|
||||
build_route_matches(router_id, base, location)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
build_route_matches(router_id, base, location)
|
||||
}
|
||||
|
||||
fn build_route_matches(
|
||||
router_id: usize,
|
||||
base: &str,
|
||||
location: String,
|
||||
) -> Rc<Vec<RouteMatch>> {
|
||||
Rc::new(Branches::with(router_id, base, |branches| {
|
||||
for branch in branches {
|
||||
if let Some(matches) = branch.matcher(&location) {
|
||||
return matches;
|
||||
}
|
||||
}
|
||||
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,
|
||||
}
|
||||
|
||||
impl Branch {
|
||||
fn matcher<'a>(&'a self, location: &'a str) -> Option<Vec<RouteMatch>> {
|
||||
let mut matches = Vec::new();
|
||||
for route in self.routes.iter().rev() {
|
||||
match route.matcher.test(location) {
|
||||
None => return None,
|
||||
Some(m) => matches.push(RouteMatch {
|
||||
path_match: m,
|
||||
route: route.clone(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
matches.reverse();
|
||||
Some(matches)
|
||||
}
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
// Implementation based on Solid Router
|
||||
// see <https://github.com/solidjs/solid-router/blob/main/src/utils.ts>
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn resolve_path<'a>(
|
||||
base: &'a str,
|
||||
path: &'a str,
|
||||
from: Option<&'a str>,
|
||||
) -> Option<Cow<'a, str>> {
|
||||
if has_scheme(path) {
|
||||
Some(path.into())
|
||||
} else {
|
||||
let base_path = normalize(base, false);
|
||||
let from_path = from.map(|from| normalize(from, false));
|
||||
let result = if let Some(from_path) = from_path {
|
||||
if path.starts_with('/') {
|
||||
base_path
|
||||
} else if from_path.to_lowercase().find(&base_path.to_lowercase())
|
||||
!= Some(0)
|
||||
{
|
||||
base_path + from_path
|
||||
} else {
|
||||
from_path
|
||||
}
|
||||
} else {
|
||||
base_path
|
||||
};
|
||||
|
||||
let result_empty = result.is_empty();
|
||||
let prefix = if result_empty { "/".into() } else { result };
|
||||
|
||||
Some(prefix + normalize(path, result_empty))
|
||||
}
|
||||
}
|
||||
|
||||
fn has_scheme(path: &str) -> bool {
|
||||
path.starts_with("//")
|
||||
|| path.starts_with("tel:")
|
||||
|| path.starts_with("mailto:")
|
||||
|| path
|
||||
.split_once("://")
|
||||
.map(|(prefix, _)| {
|
||||
prefix.chars().all(
|
||||
|c: char| matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9'),
|
||||
)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
fn normalize(path: &str, omit_slash: bool) -> Cow<'_, str> {
|
||||
let s = path.trim_start_matches('/');
|
||||
let trim_end = s
|
||||
.chars()
|
||||
.rev()
|
||||
.take_while(|c| *c == '/')
|
||||
.count()
|
||||
.saturating_sub(1);
|
||||
let s = &s[0..s.len() - trim_end];
|
||||
if s.is_empty() || omit_slash || begins_with_query_or_hash(s) {
|
||||
s.into()
|
||||
} else {
|
||||
format!("/{s}").into()
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn join_paths<'a>(from: &'a str, to: &'a str) -> String {
|
||||
let from = remove_wildcard(&normalize(from, false));
|
||||
from + normalize(to, false).as_ref()
|
||||
}
|
||||
|
||||
fn begins_with_query_or_hash(text: &str) -> bool {
|
||||
matches!(text.chars().next(), Some('#') | Some('?'))
|
||||
}
|
||||
|
||||
fn remove_wildcard(text: &str) -> String {
|
||||
text.rsplit_once('*')
|
||||
.map(|(prefix, _)| prefix)
|
||||
.unwrap_or(text)
|
||||
.trim_end_matches('/')
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn normalize_query_string_with_opening_slash() {
|
||||
assert_eq!(normalize("/?foo=bar", false), "?foo=bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_retain_trailing_slash() {
|
||||
assert_eq!(normalize("foo/bar/", false), "/foo/bar/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_dedup_trailing_slashes() {
|
||||
assert_eq!(normalize("foo/bar/////", false), "/foo/bar/");
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
use crate::{Loader, Method, SsrMode, StaticData, StaticMode, TrailingSlash};
|
||||
use leptos::leptos_dom::View;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Defines a single route in a nested route tree. This is the return
|
||||
/// type of the [`<Route/>`](crate::Route) component, but can also be
|
||||
/// used to build your own configuration-based or filesystem-based routing.
|
||||
#[derive(Clone)]
|
||||
pub struct RouteDefinition {
|
||||
/// A unique ID for each route.
|
||||
pub id: usize,
|
||||
/// The path. This can include params like `:id` or wildcards like `*all`.
|
||||
pub path: String,
|
||||
/// Other route definitions nested within this one.
|
||||
pub children: Vec<RouteDefinition>,
|
||||
/// The view that should be displayed when this route is matched.
|
||||
pub view: Rc<dyn Fn() -> View>,
|
||||
/// The mode this route prefers during server-side rendering.
|
||||
pub ssr_mode: SsrMode,
|
||||
/// The HTTP request methods this route is able to handle.
|
||||
pub methods: &'static [Method],
|
||||
/// A data loader function that will be called when this route is matched.
|
||||
pub data: Option<Loader>,
|
||||
/// The route's preferred mode of static generation, if any
|
||||
pub static_mode: Option<StaticMode>,
|
||||
/// The data required to fill any dynamic segments in the path during static rendering.
|
||||
pub static_params: Option<StaticData>,
|
||||
/// How a trailng slash in `path` should be handled.
|
||||
pub trailing_slash: Option<TrailingSlash>,
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for RouteDefinition {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("RouteDefinition")
|
||||
.field("path", &self.path)
|
||||
.field("children", &self.children)
|
||||
.field("ssr_mode", &self.ssr_mode)
|
||||
.field("static_render", &self.static_mode)
|
||||
.field("trailing_slash", &self.trailing_slash)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for RouteDefinition {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.path == other.path && self.children == other.children
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
/// Indicates which rendering mode should be used for this route during server-side rendering.
|
||||
///
|
||||
/// Leptos supports the following ways of rendering HTML that contains `async` data loaded
|
||||
/// under `<Suspense/>`.
|
||||
/// 1. **Synchronous** (use any mode except `Async`, don't depend on any resource): Serve an HTML shell that includes `fallback` for any `Suspense`. Load data on the client (using `create_local_resource`), replacing `fallback` once they're loaded.
|
||||
/// - *Pros*: App shell appears very quickly: great TTFB (time to first byte).
|
||||
/// - *Cons*: Resources load relatively slowly; you need to wait for JS + Wasm to load before even making a request.
|
||||
/// 2. **Out-of-order streaming** (`OutOfOrder`, the default): Serve an HTML shell that includes `fallback` for any `Suspense`. Load data on the **server**, streaming it down to the client as it resolves, and streaming down HTML for `Suspense` nodes.
|
||||
/// - *Pros*: Combines the best of **synchronous** and `Async`, with a very fast shell and resources that begin loading on the server.
|
||||
/// - *Cons*: Requires JS for suspended fragments to appear in correct order. Weaker meta tag support when it depends on data that's under suspense (has already streamed down `<head>`)
|
||||
/// 3. **Partially-blocked out-of-order streaming** (`PartiallyBlocked`): Using `create_blocking_resource` with out-of-order streaming still sends fallbacks and relies on JavaScript to fill them in with the fragments. Partially-blocked streaming does this replacement on the server, making for a slower response but requiring no JavaScript to show blocking resources.
|
||||
/// - *Pros*: Works better if JS is disabled.
|
||||
/// - *Cons*: Slower initial response because of additional string manipulation on server.
|
||||
/// 4. **In-order streaming** (`InOrder`): Walk through the tree, returning HTML synchronously as in synchronous rendering and out-of-order streaming until you hit a `Suspense`. At that point, wait for all its data to load, then render it, then the rest of the tree.
|
||||
/// - *Pros*: Does not require JS for HTML to appear in correct order.
|
||||
/// - *Cons*: Loads the shell more slowly than out-of-order streaming or synchronous rendering because it needs to pause at every `Suspense`. Cannot begin hydration until the entire page has loaded, so earlier pieces
|
||||
/// of the page will not be interactive until the suspended chunks have loaded.
|
||||
/// 5. **`Async`**: Load all resources on the server. Wait until all data are loaded, and render HTML in one sweep.
|
||||
/// - *Pros*: Better handling for meta tags (because you know async data even before you render the `<head>`). Faster complete load than **synchronous** because async resources begin loading on server.
|
||||
/// - *Cons*: Slower load time/TTFB: you need to wait for all async resources to load before displaying anything on the client.
|
||||
///
|
||||
/// The mode defaults to out-of-order streaming. For a path that includes multiple nested routes, the most
|
||||
/// restrictive mode will be used: i.e., if even a single nested route asks for `Async` rendering, the whole initial
|
||||
/// request will be rendered `Async`. (`Async` is the most restricted requirement, followed by `InOrder`, `PartiallyBlocked`, and `OutOfOrder`.)
|
||||
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub enum SsrMode {
|
||||
#[default]
|
||||
OutOfOrder,
|
||||
PartiallyBlocked,
|
||||
InOrder,
|
||||
Async,
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use leptos_router::expand_optionals;
|
||||
|
||||
#[test]
|
||||
fn expand_optionals_should_expand() {
|
||||
assert_eq!(expand_optionals("/foo/:x"), vec!["/foo/:x"]);
|
||||
assert_eq!(expand_optionals("/foo/:x?"), vec!["/foo", "/foo/:x"]);
|
||||
assert_eq!(expand_optionals("/bar/:x?/"), vec!["/bar/", "/bar/:x/"]);
|
||||
assert_eq!(
|
||||
expand_optionals("/foo/:x?/:y?/:z"),
|
||||
vec!["/foo/:z", "/foo/:x/:z", "/foo/:x/:y/:z"]
|
||||
);
|
||||
assert_eq!(
|
||||
expand_optionals("/foo/:x?/:y/:z?"),
|
||||
vec!["/foo/:y", "/foo/:x/:y", "/foo/:y/:z", "/foo/:x/:y/:z"]
|
||||
);
|
||||
assert_eq!(
|
||||
expand_optionals("/foo/:x?/bar/:y?/baz/:z?"),
|
||||
vec![
|
||||
"/foo/bar/baz",
|
||||
"/foo/:x/bar/baz",
|
||||
"/foo/bar/:y/baz",
|
||||
"/foo/:x/bar/:y/baz",
|
||||
"/foo/bar/baz/:z",
|
||||
"/foo/:x/bar/baz/:z",
|
||||
"/foo/bar/:y/baz/:z",
|
||||
"/foo/:x/bar/:y/baz/:z"
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
use cfg_if::cfg_if;
|
||||
|
||||
// Test cases drawn from Solid Router
|
||||
// see https://github.com/solidjs/solid-router/blob/main/test/utils.spec.ts
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use leptos_router::join_paths;
|
||||
|
||||
#[test]
|
||||
fn join_paths_should_join_with_a_single_slash() {
|
||||
assert_eq!(join_paths("/foo", "bar"), "/foo/bar");
|
||||
assert_eq!(join_paths("/foo/", "bar"), "/foo/bar");
|
||||
assert_eq!(join_paths("/foo", "/bar"), "/foo/bar");
|
||||
assert_eq!(join_paths("/foo/", "/bar"), "/foo/bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_paths_should_ensure_leading_slash() {
|
||||
assert_eq!(join_paths("/foo", ""), "/foo");
|
||||
assert_eq!(join_paths("foo", ""), "/foo");
|
||||
assert_eq!(join_paths("", "foo"), "/foo");
|
||||
assert_eq!(join_paths("", "/foo"), "/foo");
|
||||
assert_eq!(join_paths("/", "foo"), "/foo");
|
||||
assert_eq!(join_paths("/", "/foo"), "/foo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_paths_should_strip_tailing_slash_asterisk() {
|
||||
assert_eq!(join_paths("foo/*", ""), "/foo");
|
||||
assert_eq!(join_paths("foo/*", "/"), "/foo");
|
||||
assert_eq!(join_paths("/foo/*all", ""), "/foo");
|
||||
assert_eq!(join_paths("/foo/*", "bar"), "/foo/bar");
|
||||
assert_eq!(join_paths("/foo/*all", "bar"), "/foo/bar");
|
||||
assert_eq!(join_paths("/*", "foo"), "/foo");
|
||||
assert_eq!(join_paths("/*all", "foo"), "/foo");
|
||||
assert_eq!(join_paths("*", "foo"), "/foo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_paths_should_preserve_parameters() {
|
||||
assert_eq!(join_paths("/foo/:bar", ""), "/foo/:bar");
|
||||
assert_eq!(join_paths("/foo/:bar", "baz"), "/foo/:bar/baz");
|
||||
assert_eq!(join_paths("/foo", ":bar/baz"), "/foo/:bar/baz");
|
||||
assert_eq!(join_paths("", ":bar/baz"), "/:bar/baz");
|
||||
}
|
||||
|
||||
// Additional tests NOT from Solid Router:
|
||||
#[test]
|
||||
fn join_paths_for_root() {
|
||||
assert_eq!(join_paths("", ""), "");
|
||||
assert_eq!(join_paths("", "/"), "");
|
||||
assert_eq!(join_paths("/", ""), "");
|
||||
assert_eq!(join_paths("/", "/"), "");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,157 +0,0 @@
|
|||
use cfg_if::cfg_if;
|
||||
|
||||
// Test cases drawn from Solid Router
|
||||
// see https://github.com/solidjs/solid-router/blob/main/test/utils.spec.ts
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use leptos_router::{params_map, Matcher, PathMatch};
|
||||
|
||||
#[test]
|
||||
fn create_matcher_should_return_no_params_when_location_matches_exactly() {
|
||||
let matcher = Matcher::new("/foo/bar");
|
||||
let matched = matcher.test("/foo/bar");
|
||||
assert_eq!(
|
||||
matched,
|
||||
Some(PathMatch {
|
||||
path: "/foo/bar".into(),
|
||||
params: params_map!()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_matcher_should_return_none_when_location_doesnt_match() {
|
||||
let matcher = Matcher::new("/foo/bar");
|
||||
let matched = matcher.test("/foo/baz");
|
||||
assert_eq!(matched, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_matcher_should_build_params_collection() {
|
||||
let matcher = Matcher::new("/foo/:id");
|
||||
let matched = matcher.test("/foo/abc-123");
|
||||
assert_eq!(
|
||||
matched,
|
||||
Some(PathMatch {
|
||||
path: "/foo/abc-123".into(),
|
||||
params: params_map!(
|
||||
"id" => "abc-123"
|
||||
)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_matcher_should_build_params_collection_and_decode() {
|
||||
let matcher = Matcher::new("/foo/:id");
|
||||
let matched = matcher.test("/foo/%E2%89%A1abc%20123");
|
||||
assert_eq!(
|
||||
matched,
|
||||
Some(PathMatch {
|
||||
path: "/foo/%E2%89%A1abc%20123".into(),
|
||||
params: params_map!(
|
||||
"id" => "≡abc 123"
|
||||
)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_matcher_should_match_past_end_when_ending_in_asterisk() {
|
||||
let matcher = Matcher::new("/foo/bar/*");
|
||||
let matched = matcher.test("/foo/bar/baz");
|
||||
assert_eq!(
|
||||
matched,
|
||||
Some(PathMatch {
|
||||
path: "/foo/bar".into(),
|
||||
params: params_map!()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_matcher_should_not_match_past_end_when_not_ending_in_asterisk() {
|
||||
let matcher = Matcher::new("/foo/bar");
|
||||
let matched = matcher.test("/foo/bar/baz");
|
||||
assert_eq!(matched, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_matcher_should_include_remaining_unmatched_location_as_param_when_ending_in_asterisk_and_name(
|
||||
) {
|
||||
let matcher = Matcher::new("/foo/bar/*something");
|
||||
let matched = matcher.test("/foo/bar/baz/qux");
|
||||
assert_eq!(
|
||||
matched,
|
||||
Some(PathMatch {
|
||||
path: "/foo/bar".into(),
|
||||
params: params_map!(
|
||||
"something" => "baz/qux"
|
||||
)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_matcher_should_include_remaining_unmatched_location_as_param_when_ending_in_asterisk_and_name_2(
|
||||
) {
|
||||
let matcher = Matcher::new("/foo/*something");
|
||||
let matched = matcher.test("/foo/baz/qux");
|
||||
assert_eq!(
|
||||
matched,
|
||||
Some(PathMatch {
|
||||
path: "/foo".into(),
|
||||
params: params_map!(
|
||||
"something" => "baz/qux"
|
||||
)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_matcher_should_include_empty_param_when_perfect_match_ends_in_asterisk_and_name() {
|
||||
let matcher = Matcher::new("/foo/bar/*something");
|
||||
let matched = matcher.test("/foo/bar");
|
||||
assert_eq!(
|
||||
matched,
|
||||
Some(PathMatch {
|
||||
path: "/foo/bar".into(),
|
||||
params: params_map!(
|
||||
"something" => ""
|
||||
)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matcher_should_include_multiple_slashes_in_a_splat_route() {
|
||||
let matcher = Matcher::new("/*any");
|
||||
let matched = matcher.test("////");
|
||||
assert_eq!(
|
||||
matched,
|
||||
Some(PathMatch {
|
||||
path: "".into(),
|
||||
params: params_map!(
|
||||
"any" => "///"
|
||||
)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matcher_should_include_multiple_slashes_in_a_splat_route_after_others() {
|
||||
let matcher = Matcher::new("/foo/bar/*any");
|
||||
let matched = matcher.test("/foo/bar////");
|
||||
assert_eq!(
|
||||
matched,
|
||||
Some(PathMatch {
|
||||
path: "/foo/bar".into(),
|
||||
params: params_map!(
|
||||
"any" => "///"
|
||||
)
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use leptos_router::{Url, params_map};
|
||||
|
||||
macro_rules! assert_params_map {
|
||||
([$($key:expr => $val:expr),*] , $actual:expr) => (
|
||||
assert_eq!(params_map!($($key => $val),*), $actual)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_param_with_plus_sign() {
|
||||
let url = Url::try_from("http://leptos.com?data=1%2B2%3D3").unwrap();
|
||||
assert_params_map!{
|
||||
["data" => "1+2=3"],
|
||||
url.search_params
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_param_with_ampersand() {
|
||||
let url = Url::try_from("http://leptos.com?data=true+%26+false+%3D+false").unwrap();
|
||||
assert_params_map!{
|
||||
["data" => "true & false = false"],
|
||||
url.search_params
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_query_string() {
|
||||
let url = Url::try_from("http://leptos.com?data=Data%3A+%24+%26+%2B%2B+7").unwrap();
|
||||
assert_params_map!{
|
||||
["data" => "Data: $ & ++ 7"],
|
||||
url.search_params
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_query_params() {
|
||||
let url = Url::try_from("http://leptos.com?param1=value1¶m2=value2").unwrap();
|
||||
assert_params_map!{
|
||||
[
|
||||
"param1" => "value1",
|
||||
"param2" => "value2"
|
||||
],
|
||||
url.search_params
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,124 +0,0 @@
|
|||
// Test cases drawn from Solid Router
|
||||
// see https://github.com/solidjs/solid-router/blob/main/test/utils.spec.ts
|
||||
|
||||
use leptos_router::resolve_path;
|
||||
|
||||
#[test]
|
||||
fn resolve_path_should_normalize_base_arg() {
|
||||
assert_eq!(resolve_path("base", "", None), Some("/base".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_path_should_normalize_path_arg() {
|
||||
assert_eq!(resolve_path("", "path", None), Some("/path".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_path_should_normalize_from_arg() {
|
||||
assert_eq!(resolve_path("", "", Some("from")), Some("/from".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_path_should_return_default_when_all_empty() {
|
||||
assert_eq!(resolve_path("", "", None), Some("/".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_path_should_resolve_root_against_base_and_ignore_from() {
|
||||
assert_eq!(
|
||||
resolve_path("/base", "/", Some("/base/foo")),
|
||||
Some("/base".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_path_should_resolve_rooted_paths_against_base_and_ignore_from() {
|
||||
assert_eq!(
|
||||
resolve_path("/base", "/bar", Some("/base/foo")),
|
||||
Some("/base/bar".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_path_should_resolve_empty_path_against_from() {
|
||||
assert_eq!(
|
||||
resolve_path("/base", "", Some("/base/foo")),
|
||||
Some("/base/foo".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_path_should_resolve_relative_paths_against_from() {
|
||||
assert_eq!(
|
||||
resolve_path("/base", "bar", Some("/base/foo")),
|
||||
Some("/base/foo/bar".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_path_should_prepend_base_if_from_doesnt_start_with_it() {
|
||||
assert_eq!(
|
||||
resolve_path("/base", "bar", Some("/foo")),
|
||||
Some("/base/foo/bar".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_path_should_test_start_of_from_against_base_case_insensitive() {
|
||||
assert_eq!(
|
||||
resolve_path("/base", "bar", Some("BASE/foo")),
|
||||
Some("/BASE/foo/bar".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_path_should_work_with_rooted_search_and_base() {
|
||||
assert_eq!(
|
||||
resolve_path("/base", "/?foo=bar", Some("/base/page")),
|
||||
Some("/base?foo=bar".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_path_should_work_with_rooted_search() {
|
||||
assert_eq!(
|
||||
resolve_path("", "/?foo=bar", None),
|
||||
Some("/?foo=bar".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserve_spaces() {
|
||||
assert_eq!(
|
||||
resolve_path(" foo ", " bar baz ", None),
|
||||
Some("/ foo / bar baz ".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn will_resolve_if_path_has_scheme() {
|
||||
assert_eq!(
|
||||
resolve_path("", "http://example.com", None).as_deref(),
|
||||
Some("http://example.com")
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_path("", "https://example.com", None).as_deref(),
|
||||
Some("https://example.com")
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_path("", "example://google.com", None).as_deref(),
|
||||
Some("example://google.com")
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_path("", "tel:+15555555555", None).as_deref(),
|
||||
Some("tel:+15555555555")
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_path("", "mailto:name@example.com", None).as_deref(),
|
||||
Some("mailto:name@example.com")
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_path("", "//relative-protocol", None).as_deref(),
|
||||
Some("//relative-protocol")
|
||||
);
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
//! Some extra tests for Matcher NOT based on SolidJS's tests cases (as in matcher.rs)
|
||||
|
||||
use leptos_router::*;
|
||||
|
||||
#[test]
|
||||
fn trailing_slashes_match_exactly() {
|
||||
let matcher = Matcher::new("/foo/");
|
||||
assert_matches(&matcher, "/foo/");
|
||||
assert_no_match(&matcher, "/foo");
|
||||
|
||||
let matcher = Matcher::new("/foo/bar/");
|
||||
assert_matches(&matcher, "/foo/bar/");
|
||||
assert_no_match(&matcher, "/foo/bar");
|
||||
|
||||
let matcher = Matcher::new("/");
|
||||
assert_matches(&matcher, "/");
|
||||
assert_matches(&matcher, "");
|
||||
|
||||
let matcher = Matcher::new("");
|
||||
assert_matches(&matcher, "");
|
||||
|
||||
// Despite returning a pattern of "", web servers (known: Actix-Web and Axum)
|
||||
// may send us a path of "/". We should match those at the root:
|
||||
assert_matches(&matcher, "/");
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn trailing_slashes_params_match_exactly() {
|
||||
let matcher = Matcher::new("/foo/:bar/");
|
||||
assert_matches(&matcher, "/foo/bar/");
|
||||
assert_matches(&matcher, "/foo/42/");
|
||||
assert_matches(&matcher, "/foo/%20/");
|
||||
|
||||
assert_no_match(&matcher, "/foo/bar");
|
||||
assert_no_match(&matcher, "/foo/42");
|
||||
assert_no_match(&matcher, "/foo/%20");
|
||||
|
||||
let m = matcher.test("/foo/asdf/").unwrap();
|
||||
assert_eq!(m.params, params_map! { "bar" => "asdf" });
|
||||
}
|
||||
|
||||
fn assert_matches(matcher: &Matcher, path: &str) {
|
||||
assert!(
|
||||
matches(matcher, path),
|
||||
"{matcher:?} should match path {path:?}"
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_no_match(matcher: &Matcher, path: &str) {
|
||||
assert!(
|
||||
!matches(matcher, path),
|
||||
"{matcher:?} should NOT match path {path:?}"
|
||||
);
|
||||
}
|
||||
|
||||
fn matches(m: &Matcher, loc: &str) -> bool {
|
||||
m.test(loc).is_some()
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
[package]
|
||||
name = "routing_utils"
|
||||
edition = "2021"
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
either_of = { workspace = true }
|
||||
tracing = { version = "0.1", optional = true }
|
|
@ -1,26 +0,0 @@
|
|||
use crate::PathSegment;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
mod param_segments;
|
||||
mod static_segment;
|
||||
mod tuples;
|
||||
use super::PartialPathMatch;
|
||||
pub use param_segments::*;
|
||||
pub use static_segment::*;
|
||||
|
||||
/// Defines a route which may or may not be matched by any given URL,
|
||||
/// or URL segment.
|
||||
///
|
||||
/// This is a "horizontal" matching: i.e., it treats a tuple of route segments
|
||||
/// as subsequent segments of the URL and tries to match them all. For a "vertical"
|
||||
/// matching that sees a tuple as alternatives to one another, see [`RouteChild`](super::RouteChild).
|
||||
pub trait PossibleRouteMatch {
|
||||
type ParamsIter<'a>: IntoIterator<Item = (&'a str, &'a str)>;
|
||||
|
||||
fn test<'a>(
|
||||
&self,
|
||||
path: &'a str,
|
||||
) -> Option<PartialPathMatch<'a, Self::ParamsIter<'a>>>;
|
||||
|
||||
fn generate_path(&self, path: &mut Vec<PathSegment>);
|
||||
}
|
|
@ -1,139 +0,0 @@
|
|||
use super::{PartialPathMatch, PossibleRouteMatch};
|
||||
use crate::PathSegment;
|
||||
use alloc::vec::Vec;
|
||||
use core::iter;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ParamSegment(pub &'static str);
|
||||
|
||||
impl PossibleRouteMatch for ParamSegment {
|
||||
type ParamsIter<'a> = iter::Once<(&'a str, &'a str)>;
|
||||
|
||||
fn test<'a>(
|
||||
&self,
|
||||
path: &'a str,
|
||||
) -> Option<PartialPathMatch<'a, Self::ParamsIter<'a>>> {
|
||||
let mut matched_len = 0;
|
||||
let mut param_offset = 0;
|
||||
let mut param_len = 0;
|
||||
let mut test = path.chars();
|
||||
|
||||
// match an initial /
|
||||
if let Some('/') = test.next() {
|
||||
matched_len += 1;
|
||||
param_offset = 1;
|
||||
}
|
||||
for char in test {
|
||||
// when we get a closing /, stop matching
|
||||
if char == '/' {
|
||||
break;
|
||||
}
|
||||
// otherwise, push into the matched param
|
||||
else {
|
||||
matched_len += char.len_utf8();
|
||||
param_len += char.len_utf8();
|
||||
}
|
||||
}
|
||||
|
||||
let (matched, remaining) = path.split_at(matched_len);
|
||||
let param_value =
|
||||
iter::once((self.0, &path[param_offset..param_len + param_offset]));
|
||||
Some(PartialPathMatch::new(remaining, param_value, matched))
|
||||
}
|
||||
|
||||
fn generate_path(&self, path: &mut Vec<PathSegment>) {
|
||||
path.push(PathSegment::Param(self.0.into()));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct WildcardSegment(pub &'static str);
|
||||
|
||||
impl PossibleRouteMatch for WildcardSegment {
|
||||
type ParamsIter<'a> = iter::Once<(&'a str, &'a str)>;
|
||||
|
||||
fn test<'a>(
|
||||
&self,
|
||||
path: &'a str,
|
||||
) -> Option<PartialPathMatch<'a, Self::ParamsIter<'a>>> {
|
||||
let mut matched_len = 0;
|
||||
let mut param_offset = 0;
|
||||
let mut param_len = 0;
|
||||
let mut test = path.chars();
|
||||
|
||||
// match an initial /
|
||||
if let Some('/') = test.next() {
|
||||
matched_len += 1;
|
||||
param_offset += 1;
|
||||
}
|
||||
for char in test {
|
||||
matched_len += char.len_utf8();
|
||||
param_len += char.len_utf8();
|
||||
}
|
||||
|
||||
let (matched, remaining) = path.split_at(matched_len);
|
||||
let param_value =
|
||||
iter::once((self.0, &path[param_offset..param_len + param_offset]));
|
||||
Some(PartialPathMatch::new(remaining, param_value, matched))
|
||||
}
|
||||
|
||||
fn generate_path(&self, path: &mut Vec<PathSegment>) {
|
||||
path.push(PathSegment::Splat(self.0.into()));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::PossibleRouteMatch;
|
||||
use crate::{ParamSegment, StaticSegment, WildcardSegment};
|
||||
use alloc::vec::Vec;
|
||||
|
||||
#[test]
|
||||
fn single_param_match() {
|
||||
let path = "/foo";
|
||||
let def = ParamSegment("a");
|
||||
let matched = def.test(path).expect("couldn't match route");
|
||||
assert_eq!(matched.matched(), "/foo");
|
||||
assert_eq!(matched.remaining(), "");
|
||||
let params = matched.params().collect::<Vec<_>>();
|
||||
assert_eq!(params[0], ("a", "foo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_param_match_with_trailing_slash() {
|
||||
let path = "/foo/";
|
||||
let def = ParamSegment("a");
|
||||
let matched = def.test(path).expect("couldn't match route");
|
||||
assert_eq!(matched.matched(), "/foo");
|
||||
assert_eq!(matched.remaining(), "/");
|
||||
let params = matched.params().collect::<Vec<_>>();
|
||||
assert_eq!(params[0], ("a", "foo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tuple_of_param_matches() {
|
||||
let path = "/foo/bar";
|
||||
let def = (ParamSegment("a"), ParamSegment("b"));
|
||||
let matched = def.test(path).expect("couldn't match route");
|
||||
assert_eq!(matched.matched(), "/foo/bar");
|
||||
assert_eq!(matched.remaining(), "");
|
||||
let params = matched.params().collect::<Vec<_>>();
|
||||
assert_eq!(params[0], ("a", "foo"));
|
||||
assert_eq!(params[1], ("b", "bar"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn splat_should_match_all() {
|
||||
let path = "/foo/bar/////";
|
||||
let def = (
|
||||
StaticSegment("foo"),
|
||||
StaticSegment("bar"),
|
||||
WildcardSegment("rest"),
|
||||
);
|
||||
let matched = def.test(path).expect("couldn't match route");
|
||||
assert_eq!(matched.matched(), "/foo/bar/////");
|
||||
assert_eq!(matched.remaining(), "");
|
||||
let params = matched.params().collect::<Vec<_>>();
|
||||
assert_eq!(params[0], ("rest", "////"));
|
||||
}
|
||||
}
|
|
@ -1,147 +0,0 @@
|
|||
use super::{PartialPathMatch, PossibleRouteMatch};
|
||||
use crate::PathSegment;
|
||||
use alloc::vec::Vec;
|
||||
use core::iter;
|
||||
|
||||
impl PossibleRouteMatch for () {
|
||||
type ParamsIter<'a> = iter::Empty<(&'a str, &'a str)>;
|
||||
|
||||
fn test<'a>(
|
||||
&self,
|
||||
path: &'a str,
|
||||
) -> Option<PartialPathMatch<'a, Self::ParamsIter<'a>>> {
|
||||
Some(PartialPathMatch::new(path, iter::empty(), ""))
|
||||
}
|
||||
|
||||
fn generate_path(&self, _path: &mut Vec<PathSegment>) {}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct StaticSegment(pub &'static str);
|
||||
|
||||
impl PossibleRouteMatch for StaticSegment {
|
||||
type ParamsIter<'a> = iter::Empty<(&'a str, &'a str)>;
|
||||
|
||||
fn test<'a>(
|
||||
&self,
|
||||
path: &'a str,
|
||||
) -> Option<PartialPathMatch<'a, Self::ParamsIter<'a>>> {
|
||||
let mut matched_len = 0;
|
||||
let mut test = path.chars().peekable();
|
||||
let mut this = self.0.chars();
|
||||
let mut has_matched = self.0.is_empty() || self.0 == "/";
|
||||
|
||||
// match an initial /
|
||||
if let Some('/') = test.peek() {
|
||||
test.next();
|
||||
|
||||
if !self.0.is_empty() {
|
||||
matched_len += 1;
|
||||
}
|
||||
if self.0.starts_with('/') || self.0.is_empty() {
|
||||
this.next();
|
||||
}
|
||||
}
|
||||
|
||||
for char in test {
|
||||
let n = this.next();
|
||||
// when we get a closing /, stop matching
|
||||
if char == '/' || n.is_none() {
|
||||
break;
|
||||
}
|
||||
// if the next character in the path matches the
|
||||
// next character in the segment, add it to the match
|
||||
else if Some(char) == n {
|
||||
has_matched = true;
|
||||
matched_len += char.len_utf8();
|
||||
}
|
||||
// otherwise, this route doesn't match and we should
|
||||
// return None
|
||||
else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
// build the match object
|
||||
// the remaining is built from the path in, with the slice moved
|
||||
// by the length of this match
|
||||
let (matched, remaining) = path.split_at(matched_len);
|
||||
has_matched
|
||||
.then(|| PartialPathMatch::new(remaining, iter::empty(), matched))
|
||||
}
|
||||
|
||||
fn generate_path(&self, path: &mut Vec<PathSegment>) {
|
||||
path.push(PathSegment::Static(self.0.into()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{PossibleRouteMatch, StaticSegment};
|
||||
use alloc::vec::Vec;
|
||||
|
||||
#[test]
|
||||
fn single_static_match() {
|
||||
let path = "/foo";
|
||||
let def = StaticSegment("foo");
|
||||
let matched = def.test(path).expect("couldn't match route");
|
||||
assert_eq!(matched.matched(), "/foo");
|
||||
assert_eq!(matched.remaining(), "");
|
||||
let params = matched.params().collect::<Vec<_>>();
|
||||
assert!(params.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_static_mismatch() {
|
||||
let path = "/foo";
|
||||
let def = StaticSegment("bar");
|
||||
assert!(def.test(path).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_static_match_with_trailing_slash() {
|
||||
let path = "/foo/";
|
||||
let def = StaticSegment("foo");
|
||||
let matched = def.test(path).expect("couldn't match route");
|
||||
assert_eq!(matched.matched(), "/foo");
|
||||
assert_eq!(matched.remaining(), "/");
|
||||
let params = matched.params().collect::<Vec<_>>();
|
||||
assert!(params.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tuple_of_static_matches() {
|
||||
let path = "/foo/bar";
|
||||
let def = (StaticSegment("foo"), StaticSegment("bar"));
|
||||
let matched = def.test(path).expect("couldn't match route");
|
||||
assert_eq!(matched.matched(), "/foo/bar");
|
||||
assert_eq!(matched.remaining(), "");
|
||||
let params = matched.params().collect::<Vec<_>>();
|
||||
assert!(params.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tuple_static_mismatch() {
|
||||
let path = "/foo/baz";
|
||||
let def = (StaticSegment("foo"), StaticSegment("bar"));
|
||||
assert!(def.test(path).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arbitrary_nesting_of_tuples_has_no_effect_on_matching() {
|
||||
let path = "/foo/bar";
|
||||
let def = (
|
||||
(),
|
||||
(StaticSegment("foo")),
|
||||
(),
|
||||
((), ()),
|
||||
StaticSegment("bar"),
|
||||
(),
|
||||
);
|
||||
let matched = def.test(path).expect("couldn't match route");
|
||||
assert_eq!(matched.matched(), "/foo/bar");
|
||||
assert_eq!(matched.remaining(), "");
|
||||
let params = matched.params().collect::<Vec<_>>();
|
||||
assert!(params.is_empty());
|
||||
}
|
||||
}
|
|
@ -1,102 +0,0 @@
|
|||
use super::{PartialPathMatch, PathSegment, PossibleRouteMatch};
|
||||
use alloc::vec::Vec;
|
||||
use core::iter::Chain;
|
||||
|
||||
macro_rules! chain_types {
|
||||
($first:ty, $second:ty, ) => {
|
||||
Chain<
|
||||
$first,
|
||||
<<$second as PossibleRouteMatch>::ParamsIter<'a> as IntoIterator>::IntoIter
|
||||
>
|
||||
};
|
||||
($first:ty, $second:ty, $($rest:ty,)+) => {
|
||||
chain_types!(
|
||||
Chain<
|
||||
$first,
|
||||
<<$second as PossibleRouteMatch>::ParamsIter<'a> as IntoIterator>::IntoIter,
|
||||
>,
|
||||
$($rest,)+
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! tuples {
|
||||
($first:ident => $($ty:ident),*) => {
|
||||
impl<$first, $($ty),*> PossibleRouteMatch for ($first, $($ty,)*)
|
||||
where
|
||||
Self: core::fmt::Debug,
|
||||
$first: PossibleRouteMatch,
|
||||
$($ty: PossibleRouteMatch),*,
|
||||
{
|
||||
type ParamsIter<'a> = chain_types!(<<$first>::ParamsIter<'a> as IntoIterator>::IntoIter, $($ty,)*);
|
||||
|
||||
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a, Self::ParamsIter<'a>>> {
|
||||
let mut matched_len = 0;
|
||||
#[allow(non_snake_case)]
|
||||
let ($first, $($ty,)*) = &self;
|
||||
let remaining = path;
|
||||
let PartialPathMatch {
|
||||
remaining,
|
||||
matched,
|
||||
params
|
||||
} = $first.test(remaining)?;
|
||||
matched_len += matched.len();
|
||||
let params_iter = params.into_iter();
|
||||
$(
|
||||
let PartialPathMatch {
|
||||
remaining,
|
||||
matched,
|
||||
params
|
||||
} = $ty.test(remaining)?;
|
||||
matched_len += matched.len();
|
||||
let params_iter = params_iter.chain(params);
|
||||
)*
|
||||
Some(PartialPathMatch {
|
||||
remaining,
|
||||
matched: &path[0..matched_len],
|
||||
params: params_iter
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_path(&self, path: &mut Vec<PathSegment>) {
|
||||
#[allow(non_snake_case)]
|
||||
let ($first, $($ty,)*) = &self;
|
||||
$first.generate_path(path);
|
||||
$(
|
||||
$ty.generate_path(path);
|
||||
)*
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
tuples!(A => B);
|
||||
tuples!(A => B, C);
|
||||
tuples!(A => B, C, D);
|
||||
tuples!(A => B, C, D, E);
|
||||
tuples!(A => B, C, D, E, F);
|
||||
tuples!(A => B, C, D, E, F, G);
|
||||
tuples!(A => B, C, D, E, F, G, H);
|
||||
tuples!(A => B, C, D, E, F, G, H, I);
|
||||
tuples!(A => B, C, D, E, F, G, H, I, J);
|
||||
tuples!(A => B, C, D, E, F, G, H, I, J, K);
|
||||
tuples!(A => B, C, D, E, F, G, H, I, J, K, L);
|
||||
tuples!(A => B, C, D, E, F, G, H, I, J, K, L, M);
|
||||
tuples!(A => B, C, D, E, F, G, H, I, J, K, L, M, N);
|
||||
tuples!(A => B, C, D, E, F, G, H, I, J, K, L, M, N, O);
|
||||
tuples!(A => B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
|
||||
tuples!(A => B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q);
|
||||
tuples!(A => B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R);
|
||||
tuples!(A => B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S);
|
||||
tuples!(A => B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T);
|
||||
tuples!(A => B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U);
|
||||
tuples!(A => B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V);
|
||||
tuples!(A => B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W);
|
||||
tuples!(A => B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X);
|
||||
/*tuples!(
|
||||
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y
|
||||
);
|
||||
tuples!(
|
||||
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y,
|
||||
Z
|
||||
);*/
|
|
@ -1,398 +0,0 @@
|
|||
#![no_std]
|
||||
|
||||
#[macro_use]
|
||||
extern crate alloc;
|
||||
|
||||
mod path_segment;
|
||||
use alloc::vec::Vec;
|
||||
pub use path_segment::*;
|
||||
mod horizontal;
|
||||
mod nested;
|
||||
mod vertical;
|
||||
use alloc::borrow::Cow;
|
||||
pub use horizontal::*;
|
||||
pub use nested::*;
|
||||
pub use vertical::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Routes<Children> {
|
||||
base: Option<Cow<'static, str>>,
|
||||
children: Children,
|
||||
}
|
||||
|
||||
impl<Children> Routes<Children> {
|
||||
pub fn new(children: Children) -> Self {
|
||||
Self {
|
||||
base: None,
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_base(
|
||||
children: Children,
|
||||
base: impl Into<Cow<'static, str>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
base: Some(base.into()),
|
||||
children,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Children> Routes<Children>
|
||||
where
|
||||
Children: MatchNestedRoutes<'a>,
|
||||
{
|
||||
pub fn match_route(&'a self, path: &'a str) -> Option<Children::Match> {
|
||||
let path = match &self.base {
|
||||
None => path,
|
||||
Some(base) => {
|
||||
let (base, path) = if base.starts_with('/') {
|
||||
(base.trim_start_matches('/'), path.trim_start_matches('/'))
|
||||
} else {
|
||||
(base.as_ref(), path)
|
||||
};
|
||||
match path.strip_prefix(base) {
|
||||
Some(path) => path,
|
||||
None => return None,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let (matched, remaining) = self.children.match_nested(path);
|
||||
let matched = matched?;
|
||||
|
||||
if !(remaining.is_empty() || remaining == "/") {
|
||||
None
|
||||
} else {
|
||||
Some(matched.1)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_routes(
|
||||
&'a self,
|
||||
) -> (
|
||||
Option<&str>,
|
||||
impl IntoIterator<Item = Vec<PathSegment>> + 'a,
|
||||
) {
|
||||
(self.base.as_deref(), self.children.generate_routes())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct RouteMatchId(pub(crate) u8);
|
||||
|
||||
pub trait MatchInterface<'a> {
|
||||
type Params: IntoIterator<Item = (&'a str, &'a str)>;
|
||||
type Child: MatchInterface<'a>;
|
||||
type View;
|
||||
|
||||
fn as_id(&self) -> RouteMatchId;
|
||||
|
||||
fn as_matched(&self) -> &str;
|
||||
|
||||
fn to_params(&self) -> Self::Params;
|
||||
|
||||
fn into_child(self) -> Option<Self::Child>;
|
||||
|
||||
fn to_view(&self) -> Self::View;
|
||||
}
|
||||
|
||||
pub trait MatchNestedRoutes<'a> {
|
||||
type Data;
|
||||
type Match: MatchInterface<'a>;
|
||||
|
||||
fn match_nested(
|
||||
&'a self,
|
||||
path: &'a str,
|
||||
) -> (Option<(RouteMatchId, Self::Match)>, &'a str);
|
||||
|
||||
fn generate_routes(
|
||||
&self,
|
||||
) -> impl IntoIterator<Item = Vec<PathSegment>> + '_;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{NestedRoute, ParamSegment, Routes};
|
||||
use crate::{MatchInterface, PathSegment, StaticSegment, WildcardSegment};
|
||||
use alloc::vec::Vec;
|
||||
|
||||
#[test]
|
||||
pub fn matches_single_root_route() {
|
||||
let routes = Routes::new(NestedRoute {
|
||||
segments: StaticSegment("/"),
|
||||
children: (),
|
||||
data: (),
|
||||
view: || (),
|
||||
});
|
||||
let matched = routes.match_route("/");
|
||||
assert!(matched.is_some());
|
||||
let matched = routes.match_route("");
|
||||
assert!(matched.is_some());
|
||||
let (base, paths) = routes.generate_routes();
|
||||
assert_eq!(base, None);
|
||||
let paths = paths.into_iter().collect::<Vec<_>>();
|
||||
assert_eq!(paths, vec![vec![PathSegment::Static("/".into())]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn matches_nested_route() {
|
||||
let routes = Routes::new(NestedRoute {
|
||||
segments: StaticSegment(""),
|
||||
children: NestedRoute {
|
||||
segments: (StaticSegment("author"), StaticSegment("contact")),
|
||||
children: (),
|
||||
data: (),
|
||||
view: "Contact Me",
|
||||
},
|
||||
data: (),
|
||||
view: "Home",
|
||||
});
|
||||
|
||||
// route generation
|
||||
let (base, paths) = routes.generate_routes();
|
||||
assert_eq!(base, None);
|
||||
let paths = paths.into_iter().collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
paths,
|
||||
vec![vec![
|
||||
PathSegment::Static("".into()),
|
||||
PathSegment::Static("author".into()),
|
||||
PathSegment::Static("contact".into())
|
||||
]]
|
||||
);
|
||||
|
||||
let matched = routes.match_route("/author/contact").unwrap();
|
||||
assert_eq!(matched.matched(), "");
|
||||
assert_eq!(matched.into_child().unwrap().matched(), "/author/contact");
|
||||
|
||||
let matched = routes.match_route("/author/contact").unwrap();
|
||||
let view = matched.to_view();
|
||||
assert_eq!(*view, "Home");
|
||||
assert_eq!(*matched.into_child().unwrap().to_view(), "Contact Me");
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn does_not_match_incomplete_route() {
|
||||
let routes = Routes::new(NestedRoute {
|
||||
segments: StaticSegment(""),
|
||||
children: NestedRoute {
|
||||
segments: (StaticSegment("author"), StaticSegment("contact")),
|
||||
children: (),
|
||||
data: (),
|
||||
view: "Contact Me",
|
||||
},
|
||||
data: (),
|
||||
view: "Home",
|
||||
});
|
||||
let matched = routes.match_route("/");
|
||||
assert!(matched.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn chooses_between_nested_routes() {
|
||||
let routes = Routes::new((
|
||||
NestedRoute {
|
||||
segments: StaticSegment("/"),
|
||||
children: (
|
||||
NestedRoute {
|
||||
segments: StaticSegment(""),
|
||||
children: (),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
NestedRoute {
|
||||
segments: StaticSegment("about"),
|
||||
children: (),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
NestedRoute {
|
||||
segments: StaticSegment("/blog"),
|
||||
children: (
|
||||
NestedRoute {
|
||||
segments: StaticSegment(""),
|
||||
children: (),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
NestedRoute {
|
||||
segments: (StaticSegment("post"), ParamSegment("id")),
|
||||
children: (),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
));
|
||||
|
||||
// generates routes correctly
|
||||
let (base, paths) = routes.generate_routes();
|
||||
assert_eq!(base, None);
|
||||
let paths = paths.into_iter().collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
paths,
|
||||
vec![
|
||||
vec![
|
||||
PathSegment::Static("/".into()),
|
||||
PathSegment::Static("".into()),
|
||||
],
|
||||
vec![
|
||||
PathSegment::Static("/".into()),
|
||||
PathSegment::Static("about".into())
|
||||
],
|
||||
vec![
|
||||
PathSegment::Static("/blog".into()),
|
||||
PathSegment::Static("".into()),
|
||||
],
|
||||
vec![
|
||||
PathSegment::Static("/blog".into()),
|
||||
PathSegment::Static("post".into()),
|
||||
PathSegment::Param("id".into())
|
||||
]
|
||||
]
|
||||
);
|
||||
|
||||
let matched = routes.match_route("/about").unwrap();
|
||||
let params = matched.to_params().collect::<Vec<_>>();
|
||||
assert!(params.is_empty());
|
||||
let matched = routes.match_route("/blog").unwrap();
|
||||
let params = matched.to_params().collect::<Vec<_>>();
|
||||
assert!(params.is_empty());
|
||||
let matched = routes.match_route("/blog/post/42").unwrap();
|
||||
let params = matched.to_params().collect::<Vec<_>>();
|
||||
assert_eq!(params, vec![("id", "42")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn arbitrary_nested_routes() {
|
||||
let routes = Routes::new_with_base(
|
||||
(
|
||||
NestedRoute {
|
||||
segments: StaticSegment("/"),
|
||||
children: (
|
||||
NestedRoute {
|
||||
segments: StaticSegment("/"),
|
||||
children: (),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
NestedRoute {
|
||||
segments: StaticSegment("about"),
|
||||
children: (),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
NestedRoute {
|
||||
segments: StaticSegment("/blog"),
|
||||
children: (
|
||||
NestedRoute {
|
||||
segments: StaticSegment(""),
|
||||
children: (),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
NestedRoute {
|
||||
segments: StaticSegment("category"),
|
||||
children: (),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
NestedRoute {
|
||||
segments: (
|
||||
StaticSegment("post"),
|
||||
ParamSegment("id"),
|
||||
),
|
||||
children: (),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
NestedRoute {
|
||||
segments: (
|
||||
StaticSegment("/contact"),
|
||||
WildcardSegment("any"),
|
||||
),
|
||||
children: (),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
),
|
||||
"/portfolio",
|
||||
);
|
||||
|
||||
// generates routes correctly
|
||||
let (base, _paths) = routes.generate_routes();
|
||||
assert_eq!(base, Some("/portfolio"));
|
||||
|
||||
let matched = routes.match_route("/about");
|
||||
assert!(matched.is_none());
|
||||
|
||||
let matched = routes.match_route("/portfolio/about").unwrap();
|
||||
let params = matched.to_params().collect::<Vec<_>>();
|
||||
assert!(params.is_empty());
|
||||
|
||||
let matched = routes.match_route("/portfolio/blog/post/42").unwrap();
|
||||
let params = matched.to_params().collect::<Vec<_>>();
|
||||
assert_eq!(params, vec![("id", "42")]);
|
||||
|
||||
let matched = routes.match_route("/portfolio/contact").unwrap();
|
||||
let params = matched.to_params().collect::<Vec<_>>();
|
||||
assert_eq!(params, vec![("any", "")]);
|
||||
|
||||
let matched = routes.match_route("/portfolio/contact/foobar").unwrap();
|
||||
let params = matched.to_params().collect::<Vec<_>>();
|
||||
assert_eq!(params, vec![("any", "foobar")]);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PartialPathMatch<'a, ParamsIter> {
|
||||
pub(crate) remaining: &'a str,
|
||||
pub(crate) params: ParamsIter,
|
||||
pub(crate) matched: &'a str,
|
||||
}
|
||||
|
||||
impl<'a, ParamsIter> PartialPathMatch<'a, ParamsIter> {
|
||||
pub fn new(
|
||||
remaining: &'a str,
|
||||
params: ParamsIter,
|
||||
matched: &'a str,
|
||||
) -> Self {
|
||||
Self {
|
||||
remaining,
|
||||
params,
|
||||
matched,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_complete(&self) -> bool {
|
||||
self.remaining.is_empty() || self.remaining == "/"
|
||||
}
|
||||
|
||||
pub fn remaining(&self) -> &str {
|
||||
self.remaining
|
||||
}
|
||||
|
||||
pub fn params(self) -> ParamsIter {
|
||||
self.params
|
||||
}
|
||||
|
||||
pub fn matched(&self) -> &str {
|
||||
self.matched
|
||||
}
|
||||
}
|
|
@ -1,153 +0,0 @@
|
|||
use super::{
|
||||
MatchInterface, MatchNestedRoutes, PartialPathMatch, PossibleRouteMatch,
|
||||
};
|
||||
use crate::{PathSegment, RouteMatchId};
|
||||
use alloc::vec::Vec;
|
||||
use core::{fmt, iter};
|
||||
|
||||
mod tuples;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub struct NestedRoute<Segments, Children, Data, View> {
|
||||
pub segments: Segments,
|
||||
pub children: Children,
|
||||
pub data: Data,
|
||||
pub view: View,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub struct NestedMatch<'a, ParamsIter, Child, View> {
|
||||
id: RouteMatchId,
|
||||
/// The portion of the full path matched only by this nested route.
|
||||
matched: &'a str,
|
||||
/// The map of params matched only by this nested route.
|
||||
params: ParamsIter,
|
||||
/// The nested route.
|
||||
child: Child,
|
||||
view: &'a View,
|
||||
}
|
||||
|
||||
impl<'a, ParamsIter, Child, View> fmt::Debug
|
||||
for NestedMatch<'a, ParamsIter, Child, View>
|
||||
where
|
||||
ParamsIter: fmt::Debug,
|
||||
Child: fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("NestedMatch")
|
||||
.field("matched", &self.matched)
|
||||
.field("params", &self.params)
|
||||
.field("child", &self.child)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, ParamsIter, Child, View> MatchInterface<'a>
|
||||
for NestedMatch<'a, ParamsIter, Child, View>
|
||||
where
|
||||
ParamsIter: IntoIterator<Item = (&'a str, &'a str)> + Clone,
|
||||
Child: MatchInterface<'a>,
|
||||
{
|
||||
type Params = ParamsIter;
|
||||
type Child = Child;
|
||||
type View = &'a View;
|
||||
|
||||
fn as_id(&self) -> RouteMatchId {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn as_matched(&self) -> &str {
|
||||
self.matched
|
||||
}
|
||||
|
||||
fn to_params(&self) -> Self::Params {
|
||||
self.params.clone()
|
||||
}
|
||||
|
||||
fn into_child(self) -> Option<Self::Child> {
|
||||
Some(self.child)
|
||||
}
|
||||
|
||||
fn to_view(&self) -> Self::View {
|
||||
self.view
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, ParamsIter, Child, View> NestedMatch<'a, ParamsIter, Child, View> {
|
||||
pub fn matched(&self) -> &'a str {
|
||||
self.matched
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Segments, Children, Data, View> MatchNestedRoutes<'a>
|
||||
for NestedRoute<Segments, Children, Data, View>
|
||||
where
|
||||
Segments: PossibleRouteMatch,
|
||||
Children: MatchNestedRoutes<'a>,
|
||||
<Segments::ParamsIter<'a> as IntoIterator>::IntoIter: Clone,
|
||||
<<Children::Match as MatchInterface<'a>>::Params as IntoIterator>::IntoIter:
|
||||
Clone,
|
||||
Children: 'a,
|
||||
View: 'a,
|
||||
{
|
||||
type Data = Data;
|
||||
type Match = NestedMatch<'a, iter::Chain<
|
||||
<Segments::ParamsIter<'a> as IntoIterator>::IntoIter,
|
||||
<<Children::Match as MatchInterface<'a>>::Params as IntoIterator>::IntoIter,
|
||||
>, Children::Match, View>;
|
||||
|
||||
fn match_nested(
|
||||
&'a self,
|
||||
path: &'a str,
|
||||
) -> (Option<(RouteMatchId, Self::Match)>, &'a str) {
|
||||
self.segments
|
||||
.test(path)
|
||||
.and_then(
|
||||
|PartialPathMatch {
|
||||
remaining,
|
||||
params,
|
||||
matched,
|
||||
}| {
|
||||
let (inner, remaining) =
|
||||
self.children.match_nested(remaining);
|
||||
let (id, inner) = inner?;
|
||||
let params = params.into_iter();
|
||||
|
||||
if remaining.is_empty() || remaining == "/" {
|
||||
Some((
|
||||
Some((
|
||||
id,
|
||||
NestedMatch {
|
||||
id,
|
||||
matched,
|
||||
params: params.chain(inner.to_params()),
|
||||
child: inner,
|
||||
view: &self.view,
|
||||
},
|
||||
)),
|
||||
remaining,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
)
|
||||
.unwrap_or((None, path))
|
||||
}
|
||||
|
||||
fn generate_routes(
|
||||
&self,
|
||||
) -> impl IntoIterator<Item = Vec<PathSegment>> + '_ {
|
||||
let mut segment_routes = Vec::new();
|
||||
self.segments.generate_path(&mut segment_routes);
|
||||
let segment_routes = segment_routes.into_iter();
|
||||
let children_routes = self.children.generate_routes().into_iter();
|
||||
children_routes.map(move |child_routes| {
|
||||
segment_routes
|
||||
.clone()
|
||||
.chain(child_routes)
|
||||
.filter(|seg| seg != &PathSegment::Unit)
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,287 +0,0 @@
|
|||
use crate::{MatchInterface, MatchNestedRoutes, PathSegment, RouteMatchId};
|
||||
use alloc::vec::Vec;
|
||||
use core::iter;
|
||||
use either_of::*;
|
||||
|
||||
impl<'a> MatchInterface<'a> for () {
|
||||
type Params = iter::Empty<(&'a str, &'a str)>;
|
||||
type Child = ();
|
||||
type View = ();
|
||||
|
||||
fn as_id(&self) -> RouteMatchId {
|
||||
RouteMatchId(0)
|
||||
}
|
||||
|
||||
fn as_matched(&self) -> &str {
|
||||
""
|
||||
}
|
||||
|
||||
fn to_params(&self) -> Self::Params {
|
||||
iter::empty()
|
||||
}
|
||||
|
||||
fn into_child(self) -> Option<Self::Child> {
|
||||
None
|
||||
}
|
||||
|
||||
fn to_view(&self) -> Self::View {}
|
||||
}
|
||||
|
||||
impl<'a> MatchNestedRoutes<'a> for () {
|
||||
type Data = ();
|
||||
type Match = ();
|
||||
|
||||
fn match_nested(
|
||||
&self,
|
||||
path: &'a str,
|
||||
) -> (Option<(RouteMatchId, Self::Match)>, &'a str) {
|
||||
(Some((RouteMatchId(0), ())), path)
|
||||
}
|
||||
|
||||
fn generate_routes(
|
||||
&self,
|
||||
) -> impl IntoIterator<Item = Vec<PathSegment>> + '_ {
|
||||
iter::once(vec![PathSegment::Unit])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, A> MatchInterface<'a> for (A,)
|
||||
where
|
||||
A: MatchInterface<'a>,
|
||||
{
|
||||
type Params = A::Params;
|
||||
type Child = A::Child;
|
||||
type View = A::View;
|
||||
|
||||
fn as_id(&self) -> RouteMatchId {
|
||||
RouteMatchId(0)
|
||||
}
|
||||
|
||||
fn as_matched(&self) -> &str {
|
||||
self.0.as_matched()
|
||||
}
|
||||
|
||||
fn to_params(&self) -> Self::Params {
|
||||
self.0.to_params()
|
||||
}
|
||||
|
||||
fn into_child(self) -> Option<Self::Child> {
|
||||
self.0.into_child()
|
||||
}
|
||||
|
||||
fn to_view(&self) -> Self::View {
|
||||
self.0.to_view()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, A> MatchNestedRoutes<'a> for (A,)
|
||||
where
|
||||
A: MatchNestedRoutes<'a>,
|
||||
{
|
||||
type Data = A::Data;
|
||||
type Match = A::Match;
|
||||
|
||||
fn match_nested(
|
||||
&'a self,
|
||||
path: &'a str,
|
||||
) -> (Option<(RouteMatchId, Self::Match)>, &'a str) {
|
||||
self.0.match_nested(path)
|
||||
}
|
||||
|
||||
fn generate_routes(
|
||||
&self,
|
||||
) -> impl IntoIterator<Item = Vec<PathSegment>> + '_ {
|
||||
self.0.generate_routes()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, A, B> MatchInterface<'a> for Either<A, B>
|
||||
where
|
||||
A: MatchInterface<'a>,
|
||||
B: MatchInterface<'a>,
|
||||
{
|
||||
type Params = Either<
|
||||
<A::Params as IntoIterator>::IntoIter,
|
||||
<B::Params as IntoIterator>::IntoIter,
|
||||
>;
|
||||
type Child = Either<A::Child, B::Child>;
|
||||
type View = Either<A::View, B::View>;
|
||||
|
||||
fn as_id(&self) -> RouteMatchId {
|
||||
match self {
|
||||
Either::Left(_) => RouteMatchId(0),
|
||||
Either::Right(_) => RouteMatchId(1),
|
||||
}
|
||||
}
|
||||
|
||||
fn as_matched(&self) -> &str {
|
||||
match self {
|
||||
Either::Left(i) => i.as_matched(),
|
||||
Either::Right(i) => i.as_matched(),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_params(&self) -> Self::Params {
|
||||
match self {
|
||||
Either::Left(i) => Either::Left(i.to_params().into_iter()),
|
||||
Either::Right(i) => Either::Right(i.to_params().into_iter()),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_child(self) -> Option<Self::Child> {
|
||||
Some(match self {
|
||||
Either::Left(i) => Either::Left(i.into_child()?),
|
||||
Either::Right(i) => Either::Right(i.into_child()?),
|
||||
})
|
||||
}
|
||||
|
||||
fn to_view(&self) -> Self::View {
|
||||
match self {
|
||||
Either::Left(i) => Either::Left(i.to_view()),
|
||||
Either::Right(i) => Either::Right(i.to_view()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, A, B> MatchNestedRoutes<'a> for (A, B)
|
||||
where
|
||||
A: MatchNestedRoutes<'a>,
|
||||
B: MatchNestedRoutes<'a>,
|
||||
{
|
||||
type Data = (A::Data, B::Data);
|
||||
type Match = Either<A::Match, B::Match>;
|
||||
|
||||
fn match_nested(
|
||||
&'a self,
|
||||
path: &'a str,
|
||||
) -> (Option<(RouteMatchId, Self::Match)>, &'a str) {
|
||||
#[allow(non_snake_case)]
|
||||
let (A, B) = &self;
|
||||
if let (Some((id, matched)), remaining) = A.match_nested(path) {
|
||||
return (Some((id, Either::Left(matched))), remaining);
|
||||
}
|
||||
if let (Some((id, matched)), remaining) = B.match_nested(path) {
|
||||
return (Some((id, Either::Right(matched))), remaining);
|
||||
}
|
||||
(None, path)
|
||||
}
|
||||
|
||||
fn generate_routes(
|
||||
&self,
|
||||
) -> impl IntoIterator<Item = Vec<PathSegment>> + '_ {
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
let (A, B) = &self;
|
||||
|
||||
let A = A.generate_routes().into_iter();
|
||||
let B = B.generate_routes().into_iter();
|
||||
|
||||
A.chain(B)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! chain_generated {
|
||||
($first:expr, $second:expr, ) => {
|
||||
$first.chain($second)
|
||||
};
|
||||
($first:expr, $second:ident, $($rest:ident,)+) => {
|
||||
chain_generated!(
|
||||
$first.chain($second),
|
||||
$($rest,)+
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! tuples {
|
||||
($either:ident => $($ty:ident = $count:expr),*) => {
|
||||
impl<'a, $($ty,)*> MatchInterface<'a> for $either <$($ty,)*>
|
||||
where
|
||||
$($ty: MatchInterface<'a>),*,
|
||||
$($ty::Child: 'a),*,
|
||||
$($ty::View: 'a),*,
|
||||
{
|
||||
type Params = $either<$(
|
||||
<$ty::Params as IntoIterator>::IntoIter,
|
||||
)*>;
|
||||
type Child = $either<$($ty::Child,)*>;
|
||||
type View = $either<$($ty::View,)*>;
|
||||
|
||||
fn as_id(&self) -> RouteMatchId {
|
||||
match self {
|
||||
$($either::$ty(_) => RouteMatchId($count),)*
|
||||
}
|
||||
}
|
||||
|
||||
fn as_matched(&self) -> &str {
|
||||
match self {
|
||||
$($either::$ty(i) => i.as_matched(),)*
|
||||
}
|
||||
}
|
||||
|
||||
fn to_params(&self) -> Self::Params {
|
||||
match self {
|
||||
$($either::$ty(i) => $either::$ty(i.to_params().into_iter()),)*
|
||||
}
|
||||
}
|
||||
|
||||
fn into_child(self) -> Option<Self::Child> {
|
||||
Some(match self {
|
||||
$($either::$ty(i) => $either::$ty(i.into_child()?),)*
|
||||
})
|
||||
}
|
||||
|
||||
fn to_view(&self) -> Self::View {
|
||||
match self {
|
||||
$($either::$ty(i) => $either::$ty(i.to_view()),)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, $($ty),*> MatchNestedRoutes<'a> for ($($ty,)*)
|
||||
where
|
||||
$($ty: MatchNestedRoutes<'a>),*,
|
||||
$($ty::Match: 'a),*,
|
||||
{
|
||||
type Data = ($($ty::Data,)*);
|
||||
type Match = $either<$($ty::Match,)*>;
|
||||
|
||||
fn match_nested(&'a self, path: &'a str) -> (Option<(RouteMatchId, Self::Match)>, &'a str) {
|
||||
#[allow(non_snake_case)]
|
||||
|
||||
let ($($ty,)*) = &self;
|
||||
let mut id = 0;
|
||||
$(if let (Some((_, matched)), remaining) = $ty.match_nested(path) {
|
||||
return (Some((RouteMatchId(id), $either::$ty(matched))), remaining);
|
||||
} else {
|
||||
id += 1;
|
||||
})*
|
||||
(None, path)
|
||||
}
|
||||
|
||||
fn generate_routes(
|
||||
&self,
|
||||
) -> impl IntoIterator<Item = Vec<PathSegment>> + '_ {
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
let ($($ty,)*) = &self;
|
||||
$(let $ty = $ty.generate_routes().into_iter();)*
|
||||
chain_generated!($($ty,)*)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tuples!(EitherOf3 => A = 0, B = 1, C = 2);
|
||||
tuples!(EitherOf4 => A = 0, B = 1, C = 2, D = 3);
|
||||
tuples!(EitherOf5 => A = 0, B = 1, C = 2, D = 3, E = 4);
|
||||
tuples!(EitherOf6 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5);
|
||||
tuples!(EitherOf7 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6);
|
||||
tuples!(EitherOf8 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7);
|
||||
tuples!(EitherOf9 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8);
|
||||
tuples!(EitherOf10 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9);
|
||||
tuples!(EitherOf11 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10);
|
||||
tuples!(EitherOf12 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11);
|
||||
tuples!(EitherOf13 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11, M = 12);
|
||||
tuples!(EitherOf14 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11, M = 12, N = 13);
|
||||
tuples!(EitherOf15 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11, M = 12, N = 13, O = 14);
|
||||
tuples!(EitherOf16 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11, M = 12, N = 13, O = 14, P = 15);
|
|
@ -1,9 +0,0 @@
|
|||
use alloc::borrow::Cow;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum PathSegment {
|
||||
Unit,
|
||||
Static(Cow<'static, str>),
|
||||
Param(Cow<'static, str>),
|
||||
Splat(Cow<'static, str>),
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
use super::PartialPathMatch;
|
||||
|
||||
pub trait ChooseRoute {
|
||||
fn choose_route<'a>(
|
||||
&self,
|
||||
path: &'a str,
|
||||
) -> Option<
|
||||
PartialPathMatch<'a, impl IntoIterator<Item = (&'a str, &'a str)>>,
|
||||
>;
|
||||
}
|
Loading…
Reference in a new issue