mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
feat: add the ability to specify animations on route transitions (#736)
This commit is contained in:
parent
8a6d129575
commit
7382c7e51c
12 changed files with 567 additions and 45 deletions
|
@ -3,17 +3,7 @@
|
|||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z"/>
|
||||
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
|
||||
<style>
|
||||
a[aria-current] {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.contact, .contact-list {
|
||||
border: 1px solid #c0c0c0;
|
||||
border-radius: 3px;
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
||||
<link data-trunk rel="css" href="style.css"/>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
</html>
|
||||
|
|
|
@ -27,7 +27,12 @@ pub fn RouterExample(cx: Scope) -> impl IntoView {
|
|||
<A href="redirect-home">"Redirect to Home"</A>
|
||||
</nav>
|
||||
<main>
|
||||
<Routes>
|
||||
<AnimatedRoutes
|
||||
outro="slideOut"
|
||||
intro="slideIn"
|
||||
outro_back="slideOutBack"
|
||||
intro_back="slideInBack"
|
||||
>
|
||||
<ContactRoutes/>
|
||||
<Route
|
||||
path="about"
|
||||
|
@ -41,7 +46,7 @@ pub fn RouterExample(cx: Scope) -> impl IntoView {
|
|||
path="redirect-home"
|
||||
view=move |cx| view! { cx, <Redirect path="/"/> }
|
||||
/>
|
||||
</Routes>
|
||||
</AnimatedRoutes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
|
@ -102,7 +107,7 @@ pub fn ContactList(cx: Scope) -> impl IntoView {
|
|||
<Suspense fallback=move || view! { cx, <p>"Loading contacts..."</p> }>
|
||||
{move || view! { cx, <ul>{contacts}</ul>}}
|
||||
</Suspense>
|
||||
<Outlet/>
|
||||
<AnimatedOutlet outro="fadeOut" intro="fadeIn"/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
91
examples/router/style.css
Normal file
91
examples/router/style.css
Normal file
|
@ -0,0 +1,91 @@
|
|||
a[aria-current] {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.contact, .contact-list {
|
||||
border: 1px solid #c0c0c0;
|
||||
border-radius: 3px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.fadeIn {
|
||||
animation: 0.5s fadeIn forwards;
|
||||
}
|
||||
|
||||
.fadeOut {
|
||||
animation: 0.5s fadeOut forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.slideIn {
|
||||
animation: 0.125s slideIn forwards;
|
||||
}
|
||||
|
||||
.slideOut {
|
||||
animation: 0.125s slideOut forwards;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translate(100vw, 0);
|
||||
}
|
||||
to {
|
||||
transform: translate(0px, 0px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOut {
|
||||
from {
|
||||
transform: translate(0px, 0px);
|
||||
}
|
||||
to {
|
||||
transform: translate(-100vw, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.slideInBack {
|
||||
animation: 0.125s slideInBack forwards;
|
||||
}
|
||||
|
||||
.slideOutBack {
|
||||
animation: 0.125s slideOutBack forwards;
|
||||
}
|
||||
|
||||
@keyframes slideInBack {
|
||||
from {
|
||||
transform: translate(-100vw, 0);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate(0px, 0px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOutBack {
|
||||
from {
|
||||
transform: translate(0px, 0px);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate(100vw, 0);
|
||||
}
|
||||
}
|
96
router/src/animation.rs
Normal file
96
router/src/animation.rs
Normal file
|
@ -0,0 +1,96 @@
|
|||
/// 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,6 +1,10 @@
|
|||
use crate::use_route;
|
||||
use crate::{
|
||||
animation::{Animation, AnimationState},
|
||||
use_is_back_navigation, use_route,
|
||||
};
|
||||
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.
|
||||
|
@ -39,3 +43,132 @@ pub fn Outlet(cx: Scope) -> impl IntoView {
|
|||
|
||||
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(
|
||||
cx: Scope,
|
||||
/// 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(cx);
|
||||
let is_showing = Rc::new(Cell::new(None::<(usize, Scope)>));
|
||||
let (outlet, set_outlet) = create_signal(cx, None::<View>);
|
||||
|
||||
let animation = Animation {
|
||||
outro,
|
||||
start,
|
||||
intro,
|
||||
finally,
|
||||
outro_back,
|
||||
intro_back,
|
||||
};
|
||||
let (animation_state, set_animation_state) =
|
||||
create_signal(cx, AnimationState::Finally);
|
||||
let trigger_animation = create_rw_signal(cx, ());
|
||||
let is_back = use_is_back_navigation(cx);
|
||||
let animation_and_outlet = create_memo(cx, {
|
||||
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(cx, move |_| animation_and_outlet.get().0);
|
||||
let current_outlet = create_memo(cx, move |_| animation_and_outlet.get().1);
|
||||
|
||||
create_isomorphic_effect(cx, move |_| {
|
||||
match (route.child(cx), &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) => {
|
||||
if let Some(prev_scope) = prev.map(|(_, scope)| scope) {
|
||||
prev_scope.dispose();
|
||||
}
|
||||
_ = cx.child_scope(|child_cx| {
|
||||
provide_context(child_cx, child.clone());
|
||||
set_outlet
|
||||
.set(Some(child.outlet(child_cx).into_view(child_cx)));
|
||||
is_showing.set(Some((child.id(), child_cx)));
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let class = move || 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(),
|
||||
};
|
||||
let animationend = move |ev: AnimationEvent| {
|
||||
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! { cx,
|
||||
<div class=class on:animationend=animationend>
|
||||
{move || current_outlet.get()}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,6 +54,7 @@ pub(crate) struct RouterContextInner {
|
|||
referrers: Rc<RefCell<Vec<LocationChange>>>,
|
||||
state: ReadSignal<State>,
|
||||
set_state: WriteSignal<State>,
|
||||
pub(crate) is_back: RwSignal<bool>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for RouterContextInner {
|
||||
|
@ -110,6 +111,7 @@ impl RouterContext {
|
|||
replace: true,
|
||||
scroll: false,
|
||||
state: State(None),
|
||||
back: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -162,6 +164,7 @@ impl RouterContext {
|
|||
state,
|
||||
set_state,
|
||||
possible_routes: Default::default(),
|
||||
is_back: create_rw_signal(cx, false),
|
||||
});
|
||||
|
||||
// handle all click events on anchor tags
|
||||
|
@ -200,6 +203,7 @@ impl RouterContextInner {
|
|||
self: Rc<Self>,
|
||||
to: &str,
|
||||
options: &NavigateOptions,
|
||||
back: bool,
|
||||
) -> Result<(), NavigationError> {
|
||||
let cx = self.cx;
|
||||
let this = Rc::clone(&self);
|
||||
|
@ -227,6 +231,7 @@ impl RouterContextInner {
|
|||
replace: options.replace,
|
||||
scroll: options.scroll,
|
||||
state: self.state.get(),
|
||||
back,
|
||||
});
|
||||
}
|
||||
let len = self.referrers.borrow().len();
|
||||
|
@ -250,6 +255,7 @@ impl RouterContextInner {
|
|||
replace: false,
|
||||
scroll: true,
|
||||
state,
|
||||
back,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -359,8 +365,9 @@ impl RouterContextInner {
|
|||
scroll: !a.has_attribute("noscroll"),
|
||||
state: State(state),
|
||||
},
|
||||
false,
|
||||
) {
|
||||
log::error!("{e:#?}");
|
||||
leptos::error!("{e:#?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
use crate::{
|
||||
animation::*,
|
||||
matching::{
|
||||
expand_optionals, get_route_matches, join_paths, Branch, Matcher,
|
||||
RouteDefinition, RouteMatch,
|
||||
},
|
||||
RouteContext, RouterContext,
|
||||
use_is_back_navigation, RouteContext, RouterContext,
|
||||
};
|
||||
use leptos::{leptos_dom::HydrationCtx, *};
|
||||
use std::{
|
||||
|
@ -19,7 +20,9 @@ use std::{
|
|||
#[component]
|
||||
pub fn Routes(
|
||||
cx: Scope,
|
||||
#[prop(optional)] base: Option<String>,
|
||||
/// Base path relative at which the routes are mounted.
|
||||
#[prop(optional)]
|
||||
base: Option<String>,
|
||||
children: Children,
|
||||
) -> impl IntoView {
|
||||
let router = use_context::<RouterContext>(cx)
|
||||
|
@ -46,7 +49,6 @@ pub fn Routes(
|
|||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
create_branches(
|
||||
&children,
|
||||
&base.unwrap_or_default(),
|
||||
|
@ -59,20 +61,195 @@ pub fn Routes(
|
|||
*context.0.borrow_mut() = branches.clone();
|
||||
}
|
||||
|
||||
let next_route = router.pathname();
|
||||
let current_route = next_route;
|
||||
|
||||
let root_equal = Rc::new(Cell::new(true));
|
||||
let route_states =
|
||||
route_states(cx, &router, branches, current_route, &root_equal);
|
||||
|
||||
let id = HydrationCtx::id();
|
||||
let root = root_route(cx, base_route, route_states, root_equal);
|
||||
leptos::leptos_dom::DynChild::new_with_id(id, move || root.get())
|
||||
.into_view(cx)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[component]
|
||||
pub fn AnimatedRoutes(
|
||||
cx: Scope,
|
||||
/// 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>(cx)
|
||||
.expect("<Routes/> component should be nested within a <Router/>.");
|
||||
let base_route = router.base();
|
||||
|
||||
let mut branches = Vec::new();
|
||||
let frag = children(cx);
|
||||
let children = frag
|
||||
.as_children()
|
||||
.iter()
|
||||
.filter_map(|child| {
|
||||
let def = child
|
||||
.as_transparent()
|
||||
.and_then(|t| t.downcast_ref::<RouteDefinition>());
|
||||
if def.is_none() {
|
||||
warn!(
|
||||
"[NOTE] The <Routes/> component should include *only* \
|
||||
<Route/>or <ProtectedRoute/> components, or some \
|
||||
#[component(transparent)] that returns a RouteDefinition."
|
||||
);
|
||||
}
|
||||
def
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
create_branches(
|
||||
&children,
|
||||
&base.unwrap_or_default(),
|
||||
&mut Vec::new(),
|
||||
&mut branches,
|
||||
);
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
if let Some(context) = use_context::<crate::PossibleBranchContext>(cx) {
|
||||
*context.0.borrow_mut() = branches.clone();
|
||||
}
|
||||
|
||||
let animation = Animation {
|
||||
outro,
|
||||
start,
|
||||
intro,
|
||||
finally,
|
||||
outro_back,
|
||||
intro_back,
|
||||
};
|
||||
let is_back = use_is_back_navigation(cx);
|
||||
let (animation_state, set_animation_state) =
|
||||
create_signal(cx, AnimationState::Finally);
|
||||
let next_route = router.pathname();
|
||||
|
||||
let animation_and_route = create_memo(cx, {
|
||||
let branches = branches.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(|prev| get_route_matches(&branches, prev));
|
||||
let matches = get_route_matches(&branches, next_route.clone());
|
||||
let same_route = prev_matches
|
||||
.and_then(|p| p.get(0).as_ref().map(|r| r.route.key.clone()))
|
||||
== matches.get(0).as_ref().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 {
|
||||
(next_state, next_route)
|
||||
} else {
|
||||
(next_state, prev_route.to_owned())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let current_animation =
|
||||
create_memo(cx, move |_| animation_and_route.get().0);
|
||||
let current_route = create_memo(cx, move |_| animation_and_route.get().1);
|
||||
|
||||
let root_equal = Rc::new(Cell::new(true));
|
||||
let route_states =
|
||||
route_states(cx, &router, branches, current_route, &root_equal);
|
||||
|
||||
let root = root_route(cx, base_route, route_states, root_equal);
|
||||
|
||||
html::div(cx)
|
||||
.attr(
|
||||
"class",
|
||||
(cx, move || 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(),
|
||||
}),
|
||||
)
|
||||
.on(leptos::ev::animationend, move |_| {
|
||||
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(cx)
|
||||
}
|
||||
|
||||
fn route_states(
|
||||
cx: Scope,
|
||||
router: &RouterContext,
|
||||
branches: Vec<Branch>,
|
||||
current_route: Memo<String>,
|
||||
root_equal: &Rc<Cell<bool>>,
|
||||
) -> Memo<RouterState> {
|
||||
// whenever path changes, update matches
|
||||
let matches = create_memo(cx, {
|
||||
let router = router.clone();
|
||||
move |_| get_route_matches(branches.clone(), router.pathname().get())
|
||||
let matches = create_memo(cx, move |_| {
|
||||
get_route_matches(&branches, 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 root_equal = Rc::new(Cell::new(true));
|
||||
|
||||
let route_states: Memo<RouterState> = create_memo(cx, {
|
||||
let root_equal = Rc::clone(&root_equal);
|
||||
create_memo(cx, {
|
||||
let root_equal = Rc::clone(root_equal);
|
||||
move |prev: Option<&RouterState>| {
|
||||
root_equal.set(true);
|
||||
next.borrow_mut().clear();
|
||||
|
@ -114,12 +291,13 @@ pub fn Routes(
|
|||
}
|
||||
|
||||
let next = next.clone();
|
||||
let router = Rc::clone(&router.inner);
|
||||
|
||||
let next = next.clone();
|
||||
let next_ctx = RouteContext::new(
|
||||
cx,
|
||||
&RouterContext { inner: router },
|
||||
&RouterContext {
|
||||
inner: Rc::clone(&router),
|
||||
},
|
||||
{
|
||||
let next = next.clone();
|
||||
move |cx| {
|
||||
|
@ -174,12 +352,18 @@ pub fn Routes(
|
|||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
// show the root route
|
||||
let id = HydrationCtx::id();
|
||||
fn root_route(
|
||||
cx: Scope,
|
||||
base_route: RouteContext,
|
||||
route_states: Memo<RouterState>,
|
||||
root_equal: Rc<Cell<bool>>,
|
||||
) -> Memo<Option<View>> {
|
||||
let root_cx = RefCell::new(None);
|
||||
let root = create_memo(cx, move |prev| {
|
||||
|
||||
create_memo(cx, move |prev| {
|
||||
provide_context(cx, route_states);
|
||||
route_states.with(|state| {
|
||||
if state.routes.borrow().is_empty() {
|
||||
|
@ -209,9 +393,7 @@ pub fn Routes(
|
|||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
leptos::leptos_dom::DynChild::new_with_id(id, move || root.get())
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
|
|
|
@ -12,7 +12,7 @@ pub fn create_location(
|
|||
path.with(|path| match Url::try_from(path.as_str()) {
|
||||
Ok(url) => url,
|
||||
Err(e) => {
|
||||
log::error!("[Leptos Router] Invalid path {path}\n\n{e:?}");
|
||||
leptos::error!("[Leptos Router] Invalid path {path}\n\n{e:?}");
|
||||
prev.cloned().unwrap()
|
||||
}
|
||||
})
|
||||
|
@ -62,6 +62,8 @@ pub struct LocationChange {
|
|||
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,
|
||||
/// Whether the navigation is a “back” navigation.
|
||||
pub back: bool,
|
||||
}
|
||||
|
||||
impl Default for LocationChange {
|
||||
|
@ -71,6 +73,7 @@ impl Default for LocationChange {
|
|||
replace: true,
|
||||
scroll: true,
|
||||
state: Default::default(),
|
||||
back: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ pub trait History {
|
|||
pub struct BrowserIntegration {}
|
||||
|
||||
impl BrowserIntegration {
|
||||
fn current() -> LocationChange {
|
||||
fn current(back: bool) -> LocationChange {
|
||||
let loc = leptos_dom::helpers::location();
|
||||
LocationChange {
|
||||
value: loc.pathname().unwrap_or_default()
|
||||
|
@ -44,6 +44,7 @@ impl BrowserIntegration {
|
|||
replace: true,
|
||||
scroll: true,
|
||||
state: State(None), // TODO
|
||||
back,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -52,12 +53,17 @@ impl History for BrowserIntegration {
|
|||
fn location(&self, cx: Scope) -> ReadSignal<LocationChange> {
|
||||
use crate::{NavigateOptions, RouterContext};
|
||||
|
||||
let (location, set_location) = create_signal(cx, Self::current());
|
||||
let (location, set_location) = create_signal(cx, Self::current(false));
|
||||
|
||||
leptos::window_event_listener("popstate", move |_| {
|
||||
let router = use_context::<RouterContext>(cx);
|
||||
if let Some(router) = router {
|
||||
let change = Self::current();
|
||||
let is_back = router.inner.is_back;
|
||||
let change = Self::current(true);
|
||||
is_back.set(true);
|
||||
request_animation_frame(move || {
|
||||
is_back.set(false);
|
||||
});
|
||||
if let Err(e) = router.inner.navigate_from_route(
|
||||
&change.value,
|
||||
&NavigateOptions {
|
||||
|
@ -66,12 +72,13 @@ impl History for BrowserIntegration {
|
|||
scroll: change.scroll,
|
||||
state: change.state,
|
||||
},
|
||||
true,
|
||||
) {
|
||||
log::error!("{e:#?}");
|
||||
leptos::error!("{e:#?}");
|
||||
}
|
||||
set_location.set(Self::current());
|
||||
set_location.set(Self::current(true));
|
||||
} else {
|
||||
log::warn!("RouterContext not found");
|
||||
leptos::warn!("RouterContext not found");
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -165,6 +172,7 @@ impl History for ServerIntegration {
|
|||
replace: false,
|
||||
scroll: true,
|
||||
state: State(None),
|
||||
back: false,
|
||||
},
|
||||
)
|
||||
.0
|
||||
|
|
|
@ -81,6 +81,12 @@ pub fn use_navigate(
|
|||
) -> impl Fn(&str, NavigateOptions) -> Result<(), NavigationError> {
|
||||
let router = use_router(cx);
|
||||
move |to, options| {
|
||||
Rc::clone(&router.inner).navigate_from_route(to, &options)
|
||||
Rc::clone(&router.inner).navigate_from_route(to, &options, false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a signal that tells you whether you are currently navigating backwards.
|
||||
pub(crate) fn use_is_back_navigation(cx: Scope) -> ReadSignal<bool> {
|
||||
let router = use_router(cx);
|
||||
router.inner.is_back.read_only()
|
||||
}
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
#![cfg_attr(not(feature = "stable"), feature(negative_impls))]
|
||||
#![cfg_attr(not(feature = "stable"), feature(type_name_of_val))]
|
||||
|
||||
mod animation;
|
||||
mod components;
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
mod extract_routes;
|
||||
|
|
|
@ -16,7 +16,7 @@ pub(crate) struct RouteMatch {
|
|||
}
|
||||
|
||||
pub(crate) fn get_route_matches(
|
||||
branches: Vec<Branch>,
|
||||
branches: &Vec<Branch>,
|
||||
location: String,
|
||||
) -> Vec<RouteMatch> {
|
||||
for branch in branches {
|
||||
|
|
Loading…
Reference in a new issue