Prechádzať zdrojové kódy

Merge pull request #309 from Synphonyte/master

active_class prop for Router
Jon Kelley 3 rokov pred
rodič
commit
71c96a8053

+ 2 - 1
.gitignore

@@ -8,4 +8,5 @@ Cargo.lock
 !.vscode/tasks.json
 !.vscode/tasks.json
 !.vscode/launch.json
 !.vscode/launch.json
 !.vscode/extensions.json
 !.vscode/extensions.json
-tarpaulin-report.html
+tarpaulin-report.html
+.idea

+ 1 - 0
packages/router/src/cfg.rs

@@ -1,4 +1,5 @@
 #[derive(Default)]
 #[derive(Default)]
 pub struct RouterCfg {
 pub struct RouterCfg {
     pub base_url: Option<String>,
     pub base_url: Option<String>,
+    pub active_class: Option<String>,
 }
 }

+ 15 - 4
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.
     /// 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)]
     #[props(default, strip_option)]
     pub active_class: Option<&'a str>,
     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 outerlink = (*autodetect && is_http) || *external;
     let prevent_default = if outerlink { "" } else { "onclick" };
     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 route = use_route(&cx);
     let url = route.url();
     let url = route.url();
     let path = url.path();
     let path = url.path();
     let active = path == cx.props.to;
     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! {
     cx.render(rsx! {
         a {
         a {

+ 14 - 3
packages/router/src/components/router.rs

@@ -28,6 +28,13 @@ pub struct RouterProps<'a> {
     /// This lets you easily implement redirects
     /// This lets you easily implement redirects
     #[props(default)]
     #[props(default)]
     pub onchange: EventHandler<'a, Arc<RouterCore>>,
     pub onchange: EventHandler<'a, Arc<RouterCore>>,
+
+    /// 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.
 /// 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 svc = cx.use_hook(|_| {
         let (tx, mut rx) = futures_channel::mpsc::unbounded::<RouteEvent>();
         let (tx, mut rx) = futures_channel::mpsc::unbounded::<RouteEvent>();
 
 
-        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({
         cx.spawn({
             let svc = svc.clone();
             let svc = svc.clone();

+ 10 - 7
packages/router/src/hooks/use_route.rs

@@ -12,9 +12,7 @@ pub fn use_route(cx: &ScopeState) -> &UseRoute {
             .consume_context::<RouterService>()
             .consume_context::<RouterService>()
             .expect("Cannot call use_route outside the scope of a Router component");
             .expect("Cannot call use_route outside the scope of a Router component");
 
 
-        let route_context = cx
-            .consume_context::<RouteContext>()
-            .expect("Cannot call use_route outside the scope of a Router component");
+        let route_context = cx.consume_context::<RouteContext>();
 
 
         router.subscribe_onchange(cx.scope_id());
         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.
 /// A handle to the current location of the router.
 pub struct UseRoute {
 pub struct UseRoute {
     pub(crate) route: Arc<ParsedRoute>,
     pub(crate) route: Arc<ParsedRoute>,
-    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<RouteContext>,
 }
 }
 
 
 impl UseRoute {
 impl UseRoute {
@@ -84,9 +84,12 @@ impl UseRoute {
     /// `value.parse::<T>()`. This method returns `None` if the named
     /// `value.parse::<T>()`. This method returns `None` if the named
     /// parameter does not exist in the current path.
     /// parameter does not exist in the current path.
     pub fn segment(&self, name: &str) -> Option<&str> {
     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('/')
             .trim_start_matches('/')
             .split('/')
             .split('/')
             .position(|segment| segment.starts_with(':') && &segment[1..] == name)?;
             .position(|segment| segment.starts_with(':') && &segment[1..] == name)?;

+ 1 - 0
packages/router/tests/web_router.rs

@@ -22,6 +22,7 @@ fn simple_test() {
         cx.render(rsx! {
         cx.render(rsx! {
             Router {
             Router {
                 onchange: move |route: RouterService| log::info!("route changed to {:?}", route.current_location()),
                 onchange: move |route: RouterService| log::info!("route changed to {:?}", route.current_location()),
+                active_class: "is-active",
                 Route { to: "/", Home {} }
                 Route { to: "/", Home {} }
                 Route { to: "blog"
                 Route { to: "blog"
                     Route { to: "/", BlogList {} }
                     Route { to: "/", BlogList {} }

+ 19 - 0
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
 ### Segments
 
 
 Each route in your app is comprised of segments and queries. Segments are the portions of the route delimited by forward slashes.
 Each route in your app is comprised of segments and queries. Segments are the portions of the route delimited by forward slashes.