Merge pull request #309 from Synphonyte/master

active_class prop for Router
This commit is contained in:
Jon Kelley 2022-03-13 17:54:28 -04:00 committed by GitHub
commit 71c96a8053
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 62 additions and 15 deletions

1
.gitignore vendored
View file

@ -9,3 +9,4 @@ Cargo.lock
!.vscode/launch.json !.vscode/launch.json
!.vscode/extensions.json !.vscode/extensions.json
tarpaulin-report.html tarpaulin-report.html
.idea

View file

@ -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>,
} }

View file

@ -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 let active_class = if active { active_class_name } else { "".into() };
.then(|| active_class.unwrap_or("active"))
.unwrap_or("");
cx.render(rsx! { cx.render(rsx! {
a { a {

View file

@ -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,
let svc = RouterCore::new(tx, RouterCfg { base_url }); 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();

View file

@ -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 let route_context = cx.consume_context::<RouteContext>();
.consume_context::<RouteContext>()
.expect("Cannot call use_route outside the scope of a Router component");
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 let total_route = match self.route_context {
.route_context None => self.route.url.path(),
.total_route 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)?;

View file

@ -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 {} }

View file

@ -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.