link.rs 8.2 KB

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