diff --git a/.gitignore b/.gitignore index 854f5cb26..2c6aed77a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ Cargo.lock !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json -tarpaulin-report.html \ No newline at end of file +tarpaulin-report.html +.idea \ No newline at end of file diff --git a/packages/router/src/cfg.rs b/packages/router/src/cfg.rs index cec676ef2..141b03133 100644 --- a/packages/router/src/cfg.rs +++ b/packages/router/src/cfg.rs @@ -1,4 +1,5 @@ #[derive(Default)] pub struct RouterCfg { pub base_url: Option, + pub active_class: Option, } diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index 4b247be4e..5f0a2de98 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -28,7 +28,9 @@ pub struct LinkProps<'a> { /// Set the class added to the inner link when the current route is the same as the "to" route. /// - /// By default set to `"active"`. + /// To set all of the active classes inside a Router at the same time use the `active_class` + /// prop on the Router component. If both the Router prop as well as this prop are provided then + /// this one has precedence. By default set to `"active"`. #[props(default, strip_option)] pub active_class: Option<&'a str>, @@ -97,13 +99,22 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element { let outerlink = (*autodetect && is_http) || *external; let prevent_default = if outerlink { "" } else { "onclick" }; + let active_class_name = match active_class { + Some(c) => (*c).into(), + None => { + let active_from_router = match svc { + Some(service) => service.cfg.active_class.clone(), + None => None, + }; + active_from_router.unwrap_or("active".into()) + } + }; + let route = use_route(&cx); let url = route.url(); let path = url.path(); let active = path == cx.props.to; - let active_class = active - .then(|| active_class.unwrap_or("active")) - .unwrap_or(""); + let active_class = if active { active_class_name } else { "".into() }; cx.render(rsx! { a { diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index 26f17e881..d7df377f1 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -28,6 +28,13 @@ pub struct RouterProps<'a> { /// This lets you easily implement redirects #[props(default)] pub onchange: EventHandler<'a, Arc>, + + /// Set the active class of all Link components contained in this router. + /// + /// This is useful if you don't want to repeat the same `active_class` prop value in every Link. + /// By default set to `"active"`. + #[props(default, strip_option)] + pub active_class: Option<&'a str>, } /// A component that conditionally renders children based on the current location of the app. @@ -40,9 +47,13 @@ pub fn Router<'a>(cx: Scope<'a, RouterProps<'a>>) -> Element { let svc = cx.use_hook(|_| { let (tx, mut rx) = futures_channel::mpsc::unbounded::(); - let base_url = cx.props.base_url.map(|s| s.to_string()); - - let svc = RouterCore::new(tx, RouterCfg { base_url }); + let svc = RouterCore::new( + tx, + RouterCfg { + base_url: cx.props.base_url.map(|s| s.to_string()), + active_class: cx.props.active_class.map(|s| s.to_string()), + }, + ); cx.spawn({ let svc = svc.clone(); diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index 38fdd5fda..cfb1b0447 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/packages/router/src/hooks/use_route.rs @@ -12,9 +12,7 @@ pub fn use_route(cx: &ScopeState) -> &UseRoute { .consume_context::() .expect("Cannot call use_route outside the scope of a Router component"); - let route_context = cx - .consume_context::() - .expect("Cannot call use_route outside the scope of a Router component"); + let route_context = cx.consume_context::(); router.subscribe_onchange(cx.scope_id()); @@ -36,7 +34,9 @@ pub fn use_route(cx: &ScopeState) -> &UseRoute { /// A handle to the current location of the router. pub struct UseRoute { pub(crate) route: Arc, - pub(crate) route_context: RouteContext, + + /// If `use_route` is used inside a `Route` component this has some context otherwise `None`. + pub(crate) route_context: Option, } impl UseRoute { @@ -84,9 +84,12 @@ impl UseRoute { /// `value.parse::()`. This method returns `None` if the named /// parameter does not exist in the current path. pub fn segment(&self, name: &str) -> Option<&str> { - let index = self - .route_context - .total_route + let total_route = match self.route_context { + None => self.route.url.path(), + Some(ref ctx) => &ctx.total_route, + }; + + let index = total_route .trim_start_matches('/') .split('/') .position(|segment| segment.starts_with(':') && &segment[1..] == name)?; diff --git a/packages/router/tests/web_router.rs b/packages/router/tests/web_router.rs index 834baad73..d574da5bb 100644 --- a/packages/router/tests/web_router.rs +++ b/packages/router/tests/web_router.rs @@ -22,6 +22,7 @@ fn simple_test() { cx.render(rsx! { Router { onchange: move |route: RouterService| log::info!("route changed to {:?}", route.current_location()), + active_class: "is-active", Route { to: "/", Home {} } Route { to: "blog" Route { to: "/", BlogList {} } diff --git a/packages/router/usage.md b/packages/router/usage.md index 5be466fdb..58644d663 100644 --- a/packages/router/usage.md +++ b/packages/router/usage.md @@ -62,6 +62,25 @@ Link { to: "/blog/welcome", } ``` +#### Active `Links` + +When your app has been navigated to a route that matches the route of a `Link`, this `Link` becomes 'active'. +Active links have a special class attached to them. By default it is simply called `"active"` but it can be +modified on the `Link` level or on the `Router` level. Both is done through the prop `active_class`. +If the active class is given on both, the `Router` and the `Link`, the one on the `Link` has precedence. + +```rust +Router { + active_class: "custom-active", // All active links in this router get this class. + Link { to: "/", "Home" }, + Link { + to: "/blog", + active_class: "is-active", // Only for this Link. Overwrites "custom-active" from Router. + "Blog" + }, +} +``` + ### Segments Each route in your app is comprised of segments and queries. Segments are the portions of the route delimited by forward slashes.