#![cfg_attr(not(feature = "router"), allow(rustdoc::broken_intra_doc_links))]
use topcoat_core::{
context::{Cx, try_app_context, try_request_context},
error::Result,
};
use topcoat_view::{AttributeValueViewParts, Attributes, Child, View};
use topcoat_view_macro::{component, view};
const LINK_ATTRIBUTE: &str = "data-topcoat-link";
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PrefetchMode {
Never,
#[default]
Intent,
Viewport,
}
impl PrefetchMode {
const fn as_str(self) -> &'static str {
match self {
Self::Never => "never",
Self::Intent => "intent",
Self::Viewport => "viewport",
}
}
}
#[must_use]
pub fn prefetch_mode(cx: &Cx) -> PrefetchMode {
try_request_context::<PrefetchMode>(cx)
.or_else(|| try_app_context::<PrefetchMode>(cx))
.copied()
.unwrap_or_default()
}
#[must_use]
pub fn link_attrs(
cx: &Cx,
href: impl AttributeValueViewParts,
prefetch: PrefetchMode,
) -> Attributes {
let mut attrs = Attributes::with_capacity(2);
attrs.insert(cx, "href", href);
attrs.insert(cx, LINK_ATTRIBUTE, prefetch.as_str());
attrs
}
#[doc = include_str!("../docs/link.md")]
#[component]
pub async fn link<H>(
cx: &Cx,
href: H,
#[into]
#[default]
prefetch: Option<PrefetchMode>,
#[default]
mut attrs: Attributes,
#[default]
child: Child<'_>,
) -> Result<impl View>
where
H: AttributeValueViewParts + Send,
{
let prefetch = prefetch.unwrap_or_else(|| prefetch_mode(cx));
attrs.extend(link_attrs(cx, href, prefetch));
Ok(view! { <a (attrs)>(child)</a> })
}
#[cfg(test)]
mod tests {
use std::{
pin::pin,
task::{Context, Poll, Waker},
};
use topcoat::view::{ViewExt, attributes, view};
use topcoat_core::context::CxTestBuilder;
use super::*;
fn block_on<F: Future>(future: F) -> F::Output {
let mut future = pin!(future);
let mut cx = Context::from_waker(Waker::noop());
loop {
if let Poll::Ready(output) = future.as_mut().poll(&mut cx) {
return output;
}
}
}
fn render(view: impl View) -> String {
block_on(view.single()).unwrap().render(&Cx::default())
}
#[test]
fn link_attrs_mark_the_anchor_with_its_destination_and_mode() {
let cx = &Cx::default();
let attrs = link_attrs(cx, "/products", PrefetchMode::Viewport);
let html = render(view! { cx => <a (attrs)>"Products"</a> });
assert!(html.contains(r#"href="/products""#), "{html}");
assert!(html.contains(r#"data-topcoat-link="viewport""#), "{html}");
}
#[test]
fn every_mode_renders_differently() {
let cx = &Cx::default();
let mut seen = Vec::new();
for prefetch in [
PrefetchMode::Never,
PrefetchMode::Intent,
PrefetchMode::Viewport,
] {
let attrs = link_attrs(cx, "/", prefetch);
let html = render(view! { cx => <a (attrs)></a> });
assert!(!seen.contains(&html), "{html}");
seen.push(html);
}
}
#[test]
fn link_renders_an_anchor_with_forwarded_attributes() {
let cx = &Cx::default();
let attrs = attributes! { cx => class="nav" aria-current="page" };
let html = render(view! { cx => link(href: "/account", attrs: attrs, "Account") });
assert!(html.starts_with("<a "), "{html}");
assert!(html.contains(r#"href="/account""#), "{html}");
assert!(html.contains(r#"class="nav""#), "{html}");
assert!(html.contains(r#"aria-current="page""#), "{html}");
assert!(html.contains(">Account</a>"), "{html}");
assert!(html.contains(r#"data-topcoat-link="intent""#), "{html}");
}
#[test]
fn explicit_props_take_precedence_over_forwarded_attributes() {
let cx = &Cx::default();
let attrs = attributes! { cx => href="/elsewhere" data-topcoat-link="viewport" };
let html = render(view! {
cx =>
link(href: "/account", prefetch: PrefetchMode::Never, attrs: attrs)
});
assert!(html.contains(r#"href="/account""#), "{html}");
assert!(!html.contains("/elsewhere"), "{html}");
assert!(!html.contains("viewport"), "{html}");
}
#[test]
fn prefetch_mode_prefers_the_request_context_over_the_app_context() {
let app = CxTestBuilder::new()
.app_context(PrefetchMode::Viewport)
.build();
assert_eq!(prefetch_mode(&app), PrefetchMode::Viewport);
let scoped = app.with(PrefetchMode::Never);
assert_eq!(prefetch_mode(&scoped), PrefetchMode::Never);
}
#[test]
fn link_uses_the_context_mode_unless_given_one() {
let cx = &CxTestBuilder::new()
.app_context(PrefetchMode::Viewport)
.build();
let html = render(view! { cx => link(href: "/", "Home") });
assert!(html.contains(r#"data-topcoat-link="viewport""#), "{html}");
let explicit = render(view! {
cx =>
link(href: "/", prefetch: PrefetchMode::Never, "Home")
});
assert!(
explicit.contains(r#"data-topcoat-link="never""#),
"{explicit}"
);
}
}