Skip to content

Commit

Permalink
feat: Animated router transitions (#745)
Browse files Browse the repository at this point in the history
* feat: Animated router transitions

* fix: Skip DOM Nodes loaded in the same mutations run

* clean up

* clean up

* example with tabs bar

* doc comments
  • Loading branch information
marc2332 authored Jun 29, 2024
1 parent bb00890 commit e188220
Show file tree
Hide file tree
Showing 4 changed files with 514 additions and 0 deletions.
69 changes: 69 additions & 0 deletions crates/components/src/animated_router.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use dioxus::prelude::*;
use dioxus_router::prelude::{
use_route,
Routable,
};

#[derive(Clone)]
pub enum AnimatedRouterContext<R: Routable + PartialEq> {
/// Transition from one route to another.
FromTo(R, R),
/// Settled in a route.
In(R),
}

impl<R: Routable + PartialEq> AnimatedRouterContext<R> {
/// Get the current destination route.
pub fn target_route(&self) -> &R {
match self {
Self::FromTo(_, to) => to,
Self::In(to) => to,
}
}

/// Update the destination route.
pub fn set_target_route(&mut self, to: R) {
match self {
Self::FromTo(old_from, old_to) => {
*old_from = old_to.clone();
*old_to = to
}
Self::In(old_to) => *self = Self::FromTo(old_to.clone(), to),
}
}

/// After the transition animation has finished, make the outlet only render the destination route.
pub fn settle(&mut self) {
if let Self::FromTo(_, to) = self {
*self = Self::In(to.clone())
}
}
}

#[derive(Props, Clone, PartialEq)]
pub struct AnimatedRouterProps {
children: Element,
}

/// Provide a mechanism for outlets to animate between route transitions.
///
/// See the `animated_sidebar.rs` or `animated_tabs.rs` for an example on how to use it.
#[allow(non_snake_case)]
pub fn AnimatedRouter<R: Routable + PartialEq + Clone>(
AnimatedRouterProps { children }: AnimatedRouterProps,
) -> Element {
let route = use_route::<R>();
let mut prev_route = use_signal(|| AnimatedRouterContext::In(route.clone()));
use_context_provider(move || prev_route);

if prev_route.peek().target_route() != &route {
prev_route.write().set_target_route(route);
}

rsx!({ children })
}

/// Shortcut to get access to the [AnimatedRouterContext].
pub fn use_animated_router<Route: Routable + PartialEq>() -> Signal<AnimatedRouterContext<Route>> {
use_context()
}
2 changes: 2 additions & 0 deletions crates/components/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
mod accordion;
mod activable_route;
mod animated_router;
mod body;
mod button;
mod canvas;
Expand Down Expand Up @@ -39,6 +40,7 @@ mod window_drag_area;

pub use accordion::*;
pub use activable_route::*;
pub use animated_router::*;
pub use body::*;
pub use button::*;
pub use canvas::*;
Expand Down
208 changes: 208 additions & 0 deletions examples/animated_sidebar.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]

use dioxus_router::prelude::{
Outlet,
Routable,
Router,
};
use freya::prelude::*;

fn main() {
launch_with_props(app, "Animated Sidebar", (650.0, 500.0));
}

fn app() -> Element {
rsx!(Router::<Route> {})
}

#[derive(Routable, Clone, PartialEq)]
#[rustfmt::skip]
pub enum Route {
#[layout(AppSidebar)]
#[route("/")]
Home,
#[route("/wow")]
Wow,
#[route("/crab")]
Crab,
#[end_layout]
#[route("/..route")]
PageNotFound { },
}

#[component]
fn FromRouteToCurrent(from: Element, upwards: bool) -> Element {
let mut animated_router = use_animated_router::<Route>();
let (reference, node_size) = use_node();
let animations = use_animation_with_dependencies(&upwards, move |ctx, upwards| {
let (start, end) = if upwards { (1., 0.) } else { (0., 1.) };
ctx.with(
AnimNum::new(start, end)
.time(500)
.ease(Ease::Out)
.function(Function::Expo),
)
});

// Only render the destination route once the animation has finished
use_memo(move || {
if !animations.is_running() && animations.has_run_yet() {
animated_router.write().settle();
}
});

// Run the animation when any prop changes
use_memo(use_reactive((&upwards, &from), move |_| {
animations.run(AnimDirection::Forward)
}));

let offset = animations.get().read().as_f32();
let height = node_size.area.height();

let offset = height - (offset * height);
let to = rsx!(Outlet::<Route> {});
let (top, bottom) = if upwards { (from, to) } else { (to, from) };

rsx!(
rect {
reference,
height: "fill",
width: "fill",
offset_y: "-{offset}",
Expand { {top} }
Expand { {bottom} }
}
)
}

#[component]
fn Expand(children: Element) -> Element {
rsx!(
rect {
height: "100%",
width: "100%",
main_align: "center",
cross_align: "center",
{children}
}
)
}

#[component]
fn AnimatedOutlet(children: Element) -> Element {
let animated_router = use_context::<Signal<AnimatedRouterContext<Route>>>();

let from_route = match animated_router() {
AnimatedRouterContext::FromTo(Route::Home, Route::Wow) => Some((rsx!(Home {}), true)),
AnimatedRouterContext::FromTo(Route::Home, Route::Crab) => Some((rsx!(Home {}), true)),
AnimatedRouterContext::FromTo(Route::Wow, Route::Home) => Some((rsx!(Wow {}), false)),
AnimatedRouterContext::FromTo(Route::Wow, Route::Crab) => Some((rsx!(Wow {}), true)),
AnimatedRouterContext::FromTo(Route::Crab, Route::Home) => Some((rsx!(Crab {}), false)),
AnimatedRouterContext::FromTo(Route::Crab, Route::Wow) => Some((rsx!(Crab {}), false)),
_ => None,
};

if let Some((from, upwards)) = from_route {
rsx!(FromRouteToCurrent { upwards, from })
} else {
rsx!(
Expand {
Outlet::<Route> {}
}
)
}
}

#[allow(non_snake_case)]
fn AppSidebar() -> Element {
rsx!(
NativeRouter {
AnimatedRouter::<Route> {
Sidebar {
sidebar: rsx!(
Link {
to: Route::Home,
ActivableRoute {
route: Route::Home,
exact: true,
SidebarItem {
label {
"Go to Hey ! 👋"
}
},
}
},
Link {
to: Route::Wow,
ActivableRoute {
route: Route::Wow,
SidebarItem {
label {
"Go to Wow! 👈"
}
},
}
},
Link {
to: Route::Crab,
ActivableRoute {
route: Route::Crab,
SidebarItem {
label {
"Go to Crab! 🦀"
}
},
}
},
),
Body {
AnimatedOutlet { }
}
}
}
}
)
}

#[allow(non_snake_case)]
#[component]
fn Home() -> Element {
rsx!(
label {
"Just some text 😗 in /"
}
)
}

#[allow(non_snake_case)]
#[component]
fn Wow() -> Element {
rsx!(
label {
"Just more text 👈!! in /wow"
}
)
}

#[allow(non_snake_case)]
#[component]
fn Crab() -> Element {
rsx!(
label {
"🦀🦀🦀🦀🦀 /crab"
}
)
}

#[allow(non_snake_case)]
#[component]
fn PageNotFound() -> Element {
rsx!(
label {
"404!! 😵"
}
)
}
Loading

0 comments on commit e188220

Please sign in to comment.