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

3
.gitignore vendored
View file

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

View file

@ -1,4 +1,5 @@
#[derive(Default)]
pub struct RouterCfg {
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.
///
/// 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 {

View file

@ -28,6 +28,13 @@ pub struct RouterProps<'a> {
/// This lets you easily implement redirects
#[props(default)]
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.
@ -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::<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({
let svc = svc.clone();

View file

@ -12,9 +12,7 @@ pub fn use_route(cx: &ScopeState) -> &UseRoute {
.consume_context::<RouterService>()
.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());
@ -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<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 {
@ -84,9 +84,12 @@ impl UseRoute {
/// `value.parse::<T>()`. 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)?;

View file

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

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