link.rs 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. use std::any::Any;
  2. use std::fmt::Debug;
  3. use dioxus::prelude::*;
  4. use log::error;
  5. use crate::navigation::NavigationTarget;
  6. use crate::prelude::Routable;
  7. use crate::utils::use_router_internal::use_router_internal;
  8. /// Something that can be converted into a [`NavigationTarget`].
  9. pub enum IntoRoutable {
  10. /// A raw string target.
  11. FromStr(String),
  12. /// A internal target.
  13. Route(Box<dyn Any>),
  14. }
  15. impl<R: Routable> From<R> for IntoRoutable {
  16. fn from(value: R) -> Self {
  17. IntoRoutable::Route(Box::new(value) as Box<dyn Any>)
  18. }
  19. }
  20. impl<R: Routable> From<NavigationTarget<R>> for IntoRoutable {
  21. fn from(value: NavigationTarget<R>) -> Self {
  22. match value {
  23. NavigationTarget::Internal(route) => {
  24. IntoRoutable::Route(Box::new(route) as Box<dyn Any>)
  25. }
  26. NavigationTarget::External(url) => IntoRoutable::FromStr(url),
  27. }
  28. }
  29. }
  30. impl From<String> for IntoRoutable {
  31. fn from(value: String) -> Self {
  32. IntoRoutable::FromStr(value)
  33. }
  34. }
  35. impl From<&String> for IntoRoutable {
  36. fn from(value: &String) -> Self {
  37. IntoRoutable::FromStr(value.to_string())
  38. }
  39. }
  40. impl From<&str> for IntoRoutable {
  41. fn from(value: &str) -> Self {
  42. IntoRoutable::FromStr(value.to_string())
  43. }
  44. }
  45. /// The properties for a [`Link`].
  46. #[derive(Props)]
  47. pub struct LinkProps<'a> {
  48. /// A class to apply to the generate HTML anchor tag if the `target` route is active.
  49. pub active_class: Option<&'a str>,
  50. /// The children to render within the generated HTML anchor tag.
  51. pub children: Element<'a>,
  52. /// The class attribute for the generated HTML anchor tag.
  53. ///
  54. /// If `active_class` is [`Some`] and the `target` route is active, `active_class` will be
  55. /// appended at the end of `class`.
  56. pub class: Option<&'a str>,
  57. /// The id attribute for the generated HTML anchor tag.
  58. pub id: Option<&'a str>,
  59. /// When [`true`], the `target` route will be opened in a new tab.
  60. ///
  61. /// This does not change whether the [`Link`] is active or not.
  62. #[props(default)]
  63. pub new_tab: bool,
  64. /// The onclick event handler.
  65. pub onclick: Option<EventHandler<'a, MouseEvent>>,
  66. #[props(default)]
  67. /// Whether the default behavior should be executed if an `onclick` handler is provided.
  68. ///
  69. /// 1. When `onclick` is [`None`] (default if not specified), `onclick_only` has no effect.
  70. /// 2. If `onclick_only` is [`false`] (default if not specified), the provided `onclick` handler
  71. /// will be executed after the links regular functionality.
  72. /// 3. If `onclick_only` is [`true`], only the provided `onclick` handler will be executed.
  73. pub onclick_only: bool,
  74. /// The rel attribute for the generated HTML anchor tag.
  75. ///
  76. /// For external `target`s, this defaults to `noopener noreferrer`.
  77. pub rel: Option<&'a str>,
  78. /// The navigation target. Roughly equivalent to the href attribute of an HTML anchor tag.
  79. #[props(into)]
  80. pub to: IntoRoutable,
  81. }
  82. impl Debug for LinkProps<'_> {
  83. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  84. f.debug_struct("LinkProps")
  85. .field("active_class", &self.active_class)
  86. .field("children", &self.children)
  87. .field("class", &self.class)
  88. .field("id", &self.id)
  89. .field("new_tab", &self.new_tab)
  90. .field("onclick", &self.onclick.as_ref().map(|_| "onclick is set"))
  91. .field("onclick_only", &self.onclick_only)
  92. .field("rel", &self.rel)
  93. .finish()
  94. }
  95. }
  96. /// A link to navigate to another route.
  97. ///
  98. /// Only works as descendant of a [`Router`] component, otherwise it will be inactive.
  99. ///
  100. /// Unlike a regular HTML anchor, a [`Link`] allows the router to handle the navigation and doesn't
  101. /// cause the browser to load a new page.
  102. ///
  103. /// However, in the background a [`Link`] still generates an anchor, which you can use for styling
  104. /// as normal.
  105. ///
  106. /// # External targets
  107. /// When the [`Link`]s target is an [`NavigationTarget::External`] target, that is used as the `href` directly. This
  108. /// means that a [`Link`] can always navigate to an [`NavigationTarget::External`] target, even if the [`HistoryProvider`] does not support it.
  109. ///
  110. /// # Panic
  111. /// - When the [`Link`] is not nested within a [`Router`], but
  112. /// only in debug builds.
  113. ///
  114. /// # Example
  115. /// ```rust
  116. /// # use dioxus::prelude::*;
  117. /// # use dioxus_router::prelude::*;
  118. ///
  119. /// #[derive(Clone, Routable)]
  120. /// enum Route {
  121. /// #[route("/")]
  122. /// Index {},
  123. /// }
  124. ///
  125. /// fn App(cx: Scope) -> Element {
  126. /// render! {
  127. /// Router::<Route> {}
  128. /// }
  129. /// }
  130. ///
  131. /// #[inline_props]
  132. /// fn Index(cx: Scope) -> Element {
  133. /// render! {
  134. /// render! {
  135. /// Link {
  136. /// active_class: "active",
  137. /// class: "link_class",
  138. /// id: "link_id",
  139. /// new_tab: true,
  140. /// rel: "link_rel",
  141. /// to: Route::Index {},
  142. ///
  143. /// "A fully configured link"
  144. /// }
  145. /// }
  146. /// }
  147. /// }
  148. /// #
  149. /// # let mut vdom = VirtualDom::new(App);
  150. /// # let _ = vdom.rebuild();
  151. /// # assert_eq!(
  152. /// # dioxus_ssr::render(&vdom),
  153. /// # r#"<a href="/" dioxus-prevent-default="" class="link_class active" id="link_id" rel="link_rel" target="_blank">A fully configured link</a>"#
  154. /// # );
  155. /// ```
  156. #[allow(non_snake_case)]
  157. pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
  158. let LinkProps {
  159. active_class,
  160. children,
  161. class,
  162. id,
  163. new_tab,
  164. onclick,
  165. onclick_only,
  166. rel,
  167. to,
  168. ..
  169. } = cx.props;
  170. // hook up to router
  171. let router = match use_router_internal(cx) {
  172. Some(r) => r,
  173. #[allow(unreachable_code)]
  174. None => {
  175. let msg = "`Link` must have access to a parent router";
  176. error!("{msg}, will be inactive");
  177. #[cfg(debug_assertions)]
  178. panic!("{}", msg);
  179. return None;
  180. }
  181. };
  182. let current_url = router.current_route_string();
  183. let href = match to {
  184. IntoRoutable::FromStr(url) => url.to_string(),
  185. IntoRoutable::Route(route) => router.any_route_to_string(&**route),
  186. };
  187. let parsed_route: NavigationTarget<Box<dyn Any>> = match router.route_from_str(&href) {
  188. Ok(route) => NavigationTarget::Internal(route.into()),
  189. Err(err) => NavigationTarget::External(err),
  190. };
  191. let ac = active_class
  192. .and_then(|active_class| (href == current_url).then(|| format!(" {active_class}")))
  193. .unwrap_or_default();
  194. let id = id.unwrap_or_default();
  195. let class = format!("{}{ac}", class.unwrap_or_default());
  196. let tag_target = new_tab.then_some("_blank").unwrap_or_default();
  197. let is_external = matches!(parsed_route, NavigationTarget::External(_));
  198. let is_router_nav = !is_external && !new_tab;
  199. let prevent_default = is_router_nav.then_some("onclick").unwrap_or_default();
  200. let rel = rel
  201. .or_else(|| is_external.then_some("noopener noreferrer"))
  202. .unwrap_or_default();
  203. let do_default = onclick.is_none() || !onclick_only;
  204. let action = move |event| {
  205. if do_default && is_router_nav {
  206. let href = match to {
  207. IntoRoutable::FromStr(url) => url.to_string(),
  208. IntoRoutable::Route(route) => router.any_route_to_string(&**route),
  209. };
  210. let parsed_route: NavigationTarget<Box<dyn Any>> = match router.route_from_str(&href) {
  211. Ok(route) => NavigationTarget::Internal(route.into()),
  212. Err(err) => NavigationTarget::External(err),
  213. };
  214. router.push_any(parsed_route);
  215. }
  216. if let Some(handler) = onclick {
  217. handler.call(event);
  218. }
  219. };
  220. render! {
  221. a {
  222. onclick: action,
  223. href: "{href}",
  224. prevent_default: "{prevent_default}",
  225. class: "{class}",
  226. id: "{id}",
  227. rel: "{rel}",
  228. target: "{tag_target}",
  229. children
  230. }
  231. }
  232. }