Merge pull request #231 from gbj/router-changes

Close issue #229 and update router docs
This commit is contained in:
Greg Johnston 2023-01-03 21:51:22 -05:00 committed by GitHub
commit 632267c13a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 93 additions and 27 deletions

View File

@ -10,7 +10,8 @@ pub extern crate tracing;
mod components;
mod events;
mod helpers;
mod html;
#[doc(hidden)]
pub mod html;
mod hydration;
mod logging;
mod macro_helpers;

View File

@ -88,7 +88,7 @@ where
error.set(None);
}
if let Some(on_response) = on_response.clone() {
on_response(&resp.as_raw());
on_response(resp.as_raw());
}
if resp.status() == 303 {
@ -264,7 +264,7 @@ fn extract_form_attributes(
.unwrap_or_else(|| "get".to_string())
.to_lowercase(),
form.get_attribute("action")
.unwrap_or_else(|| "".to_string())
.unwrap_or_default()
.to_lowercase(),
form.get_attribute("enctype")
.unwrap_or_else(|| "application/x-www-form-urlencoded".to_string())
@ -284,7 +284,7 @@ fn extract_form_attributes(
}),
input.get_attribute("action").unwrap_or_else(|| {
form.get_attribute("action")
.unwrap_or_else(|| "".to_string())
.unwrap_or_default()
.to_lowercase()
}),
input.get_attribute("enctype").unwrap_or_else(|| {
@ -307,7 +307,7 @@ fn extract_form_attributes(
}),
button.get_attribute("action").unwrap_or_else(|| {
form.get_attribute("action")
.unwrap_or_else(|| "".to_string())
.unwrap_or_default()
.to_lowercase()
}),
button.get_attribute("enctype").unwrap_or_else(|| {
@ -344,7 +344,7 @@ fn extract_form_attributes(
fn action_input_from_form_data<I: serde::de::DeserializeOwned>(
form_data: &web_sys::FormData,
) -> Result<I, serde_urlencoded::de::Error> {
let data = web_sys::UrlSearchParams::new_with_str_sequence_sequence(&form_data).unwrap_throw();
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_urlencoded::from_str::<I>(&data)
}

View File

@ -86,7 +86,7 @@ where
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
view! { cx,
<a
<html::a
href=move || href.get().unwrap_or_default()
prop:state={state.map(|s| s.to_js_value())}
prop:replace={replace}
@ -94,17 +94,17 @@ where
class=move || class.as_ref().map(|class| class.get())
>
{children(cx)}
</a>
</html::a>
}
} else {
view! { cx,
<a
<html::a
href=move || href.get().unwrap_or_default()
aria-current=move || if is_active.get() { Some("page") } else { None }
class=move || class.as_ref().map(|class| class.get())
>
{children(cx)}
</a>
</html::a>
}
}
}

View File

@ -1,4 +1,4 @@
use std::{cell::RefCell, rc::Rc};
use std::{cell::Cell, rc::Rc};
use crate::use_route;
use leptos::*;
@ -8,21 +8,18 @@ use leptos::*;
#[component]
pub fn Outlet(cx: Scope) -> impl IntoView {
let route = use_route(cx);
let is_showing = Rc::new(RefCell::new(None));
let is_showing = Rc::new(Cell::new(None));
let (outlet, set_outlet) = create_signal(cx, None);
create_effect(cx, move |_| {
let is_showing_val = { is_showing.borrow().clone() };
match (route.child(), &is_showing_val) {
match (route.child(), &is_showing.get()) {
(None, _) => {
set_outlet.set(None);
}
(Some(child), Some(_))
if Some(child.original_path().to_string()) == is_showing_val =>
{
(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
}
(Some(child), _) => {
*is_showing.borrow_mut() = Some(child.original_path().to_string());
is_showing.set(Some(child.id()));
provide_context(child.cx(), child.clone());
set_outlet.set(Some(child.outlet().into_view(cx)))
}

View File

@ -1,4 +1,4 @@
use std::{borrow::Cow, rc::Rc};
use std::{borrow::Cow, cell::Cell, rc::Rc};
use leptos::*;
@ -7,15 +7,19 @@ use crate::{
ParamsMap, RouterContext,
};
thread_local! {
static ROUTE_ID: Cell<usize> = Cell::new(0);
}
/// 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.
#[component(transparent)]
pub fn Route<E, F>(
pub fn Route<E, F, P>(
cx: Scope,
/// 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: &'static str,
path: P,
/// The view that should be shown when this route is matched. This can be any function
/// that takes a [Scope] and returns an [Element] (like `|cx| view! { cx, <p>"Show this"</p> })`
/// or `|cx| view! { cx, <MyComponent/>` } or even, for a component with no props, `MyComponent`).
@ -27,6 +31,7 @@ pub fn Route<E, F>(
where
E: IntoView,
F: Fn(Scope) -> E + 'static,
P: std::fmt::Display,
{
let children = children
.map(|children| {
@ -42,8 +47,14 @@ where
.collect::<Vec<_>>()
})
.unwrap_or_default();
let id = ROUTE_ID.with(|id| {
let next = id.get() + 1;
id.set(next);
next
});
RouteDefinition {
path,
id,
path: path.to_string(),
children,
view: Rc::new(move |cx| view(cx).into_view(cx)),
}
@ -72,7 +83,9 @@ impl RouteContext {
let base = base.path();
let RouteMatch { path_match, route } = matcher()?;
let PathMatch { path, .. } = path_match;
let RouteDefinition { view: element, .. } = route.key;
let RouteDefinition {
view: element, id, ..
} = route.key;
let params = create_memo(cx, move |_| {
matcher()
.map(|matched| matched.path_match.params)
@ -82,6 +95,7 @@ impl RouteContext {
Some(Self {
inner: Rc::new(RouteContextInner {
cx,
id,
base_path: base.to_string(),
child: Box::new(child),
path,
@ -97,6 +111,10 @@ impl RouteContext {
self.inner.cx
}
pub(crate) fn id(&self) -> usize {
self.inner.id
}
/// Returns the URL path of the current route,
/// including param values in their places.
///
@ -124,6 +142,7 @@ impl RouteContext {
Self {
inner: Rc::new(RouteContextInner {
cx,
id: 0,
base_path: path.to_string(),
child: Box::new(|| None),
path: path.to_string(),
@ -153,6 +172,7 @@ impl RouteContext {
pub(crate) struct RouteContextInner {
cx: Scope,
base_path: String,
pub(crate) id: usize,
pub(crate) child: Box<dyn Fn() -> Option<RouteContext>>,
pub(crate) path: String,
pub(crate) original_path: String,

View File

@ -86,7 +86,8 @@ pub fn Routes(
match (prev_routes, prev_match) {
(Some(prev), Some(prev_match))
if next_match.route.key == prev_match.route.key =>
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 i >= next.borrow().len() {
@ -212,6 +213,7 @@ struct RouterState {
#[derive(Debug, Clone, PartialEq)]
pub struct RouteData {
pub id: usize,
pub key: RouteDefinition,
pub pattern: String,
pub original_path: String,
@ -228,6 +230,7 @@ impl RouteData {
.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 },
@ -273,7 +276,7 @@ fn create_routes(route_def: &RouteDefinition, base: &str) -> Vec<RouteData> {
let RouteDefinition { children, .. } = route_def;
let is_leaf = children.is_empty();
let mut acc = Vec::new();
for original_path in expand_optionals(route_def.path) {
for original_path in expand_optionals(&route_def.path) {
let path = join_paths(base, &original_path);
let pattern = if is_leaf {
path
@ -285,6 +288,7 @@ fn create_routes(route_def: &RouteDefinition, base: &str) -> Vec<RouteData> {
};
acc.push(RouteData {
key: route_def.clone(),
id: route_def.id,
matcher: Matcher::new_with_partial(&pattern, !is_leaf),
pattern,
original_path: original_path.to_string(),

View File

@ -64,7 +64,7 @@ pub fn use_resolved_path(cx: Scope, path: impl Fn() -> String + 'static) -> Memo
create_memo(cx, move |_| {
let path = path();
if path.starts_with("/") {
if path.starts_with('/') {
Some(path)
} else {
route.resolve_path(&path).map(String::from)

View File

@ -135,6 +135,49 @@
//! }
//!
//! ```
//!
//! ## 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(cx: Scope) -> impl IntoView {
//! view! { cx,
//! <Router>
//! <Routes>
//! <Route path="/" view=move |cx| {
//! view! { cx, "-> /" }
//! }/>
//! <ExternallyDefinedRoute/>
//! </Routes>
//! </Router>
//! }
//! }
//!
//! // `transparent` here marks the component as returning data (a RouteDefinition), not a view
//! #[component(transparent)]
//! pub fn ExternallyDefinedRoute(cx: Scope) -> impl IntoView {
//! view! { cx,
//! <Route path="/some-area" view=move |cx| {
//! view! { cx, <div>
//! <h2>"Some Area"</h2>
//! <Outlet/>
//! </div> }
//! }>
//! <Route path="/path-a/:id" view=move |cx| {
//! view! { cx, <p>"Path A"</p> }
//! }/>
//! <Route path="/path-b/:id" view=move |cx| {
//! view! { cx, <p>"Path B"</p> }
//! }/>
//! </Route>
//! }
//! }
//! ```
#![cfg_attr(not(feature = "stable"), feature(auto_traits))]
#![cfg_attr(not(feature = "stable"), feature(negative_impls))]

View File

@ -5,7 +5,8 @@ use leptos::*;
#[derive(Clone)]
pub struct RouteDefinition {
pub path: &'static str,
pub id: usize,
pub path: String,
pub children: Vec<RouteDefinition>,
pub view: Rc<dyn Fn(Scope) -> View>,
}