mirror of
https://github.com/leptos-rs/leptos
synced 2024-09-20 14:32:00 +00:00
Add <Stylesheet> component to meta
This commit is contained in:
parent
6365ac2c8c
commit
b2fcb552ea
14 changed files with 293 additions and 111 deletions
|
@ -19,6 +19,6 @@ pub fn main() {
|
|||
Todo::new(cx, 2, "Profit!".to_string()),
|
||||
]);
|
||||
|
||||
view! { <main><TodoMVC todos=todos/></main> }
|
||||
view! { <TodoMVC todos=todos/> }
|
||||
});
|
||||
}
|
||||
|
|
|
@ -31,9 +31,7 @@ async fn render_todomvc() -> impl Responder {
|
|||
]);
|
||||
|
||||
view! {
|
||||
<main>
|
||||
<TodoMVC todos=todos/>
|
||||
</main>
|
||||
<TodoMVC todos=todos/>
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -19,7 +19,7 @@ lto = true
|
|||
opt-level = 'z'
|
||||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
default = ["hydrate"]
|
||||
csr = ["leptos/csr"]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = ["leptos/ssr"]
|
||||
|
|
|
@ -109,7 +109,7 @@ const ESCAPE_KEY: u32 = 27;
|
|||
const ENTER_KEY: u32 = 13;
|
||||
|
||||
#[component]
|
||||
pub fn TodoMVC(cx: Scope, todos: Todos) -> Vec<Element> {
|
||||
pub fn TodoMVC(cx: Scope, todos: Todos) -> Element {
|
||||
let mut next_id = todos
|
||||
.0
|
||||
.iter()
|
||||
|
@ -178,7 +178,7 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Vec<Element> {
|
|||
});
|
||||
|
||||
view! {
|
||||
<>
|
||||
<main>
|
||||
<section class="todoapp">
|
||||
<header class="header">
|
||||
<h1>"todos"</h1>
|
||||
|
@ -225,7 +225,7 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Vec<Element> {
|
|||
<p>"Created by "<a href="http://todomvc.com">"Greg Johnston"</a></p>
|
||||
<p>"Part of "<a href="http://todomvc.com">"TodoMVC"</a></p>
|
||||
</footer>
|
||||
</>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ use std::time::Duration;
|
|||
|
||||
use wasm_bindgen::{prelude::Closure, JsCast, JsValue, UnwrapThrowExt};
|
||||
|
||||
use crate::{event_delegation, is_server};
|
||||
use crate::{debug_warn, event_delegation, is_server};
|
||||
|
||||
thread_local! {
|
||||
pub static WINDOW: web_sys::Window = web_sys::window().unwrap_throw();
|
||||
|
@ -71,7 +71,31 @@ pub fn insert_before(
|
|||
new: &web_sys::Node,
|
||||
existing: Option<&web_sys::Node>,
|
||||
) -> web_sys::Node {
|
||||
parent.insert_before(new, existing).unwrap_throw()
|
||||
log::warn!(
|
||||
"insert_before insert {} before {:?} \n\non {}",
|
||||
new.node_name(),
|
||||
existing.map(|e| e.node_name()),
|
||||
parent.node_name(),
|
||||
);
|
||||
if parent.node_type() != 1 {
|
||||
debug_warn!("insert_before: trying to insert on a parent node that is not an element");
|
||||
new.clone()
|
||||
} else if let Some(existing) = existing {
|
||||
if existing.parent_node().as_ref() == Some(parent.unchecked_ref()) {
|
||||
match parent.insert_before(new, Some(existing)) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
log::warn!("{:?}", e.as_string());
|
||||
new.clone()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug_warn!("insert_before: existing node is not a child of parent node");
|
||||
parent.append_child(new).unwrap_throw()
|
||||
}
|
||||
} else {
|
||||
parent.append_child(new).unwrap_throw()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn replace_with(old_node: &web_sys::Element, new_node: &web_sys::Node) {
|
||||
|
|
|
@ -26,9 +26,10 @@ pub fn reconcile_arrays(parent: &web_sys::Element, a: &mut [web_sys::Node], b: &
|
|||
{
|
||||
for (i, node) in a.iter().enumerate() {
|
||||
if node.parent_node().as_ref() != Some(parent) {
|
||||
panic!(
|
||||
crate::debug_warn!(
|
||||
"node {} in existing nodes Vec is not a child of parent. node = {:#?}",
|
||||
i, node
|
||||
i,
|
||||
node
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -81,8 +82,8 @@ pub fn reconcile_arrays(parent: &web_sys::Element, a: &mut [web_sys::Node], b: &
|
|||
} else if a[a_start] == b[b_end - 1] && b[b_start] == a[a_end - 1] {
|
||||
// Swap backwards.
|
||||
let node = a[a_end - 1].next_sibling();
|
||||
parent.insert_before(&b[b_start], a[a_start].next_sibling().as_ref());
|
||||
parent.insert_before(&b[b_end - 1], node.as_ref());
|
||||
insert_before(parent, &b[b_start], a[a_start].next_sibling().as_ref());
|
||||
insert_before(parent, &b[b_end - 1], node.as_ref());
|
||||
a_start += 1;
|
||||
b_start += 1;
|
||||
a_end -= 1;
|
||||
|
@ -119,7 +120,7 @@ pub fn reconcile_arrays(parent: &web_sys::Element, a: &mut [web_sys::Node], b: &
|
|||
if sequence > index - b_start {
|
||||
let node = &a[a_start];
|
||||
while b_start < index {
|
||||
parent.insert_before(&b[b_start], Some(node));
|
||||
insert_before(parent, &b[b_start], Some(node));
|
||||
b_start += 1;
|
||||
}
|
||||
} else {
|
||||
|
@ -142,7 +143,7 @@ pub fn reconcile_arrays(parent: &web_sys::Element, a: &mut [web_sys::Node], b: &
|
|||
{
|
||||
for (i, node) in b.iter().enumerate() {
|
||||
if node.parent_node().as_ref() != Some(parent) {
|
||||
panic!(
|
||||
crate::debug_warn!(
|
||||
"node {} in new nodes Vec is not a child of parent after reconciliation. node = {:#?}",
|
||||
i, node
|
||||
);
|
||||
|
|
|
@ -132,6 +132,12 @@ pub fn insert(
|
|||
"inserting {value:?} on {} before {before:?} with initial = {initial:?}",
|
||||
parent.node_name()
|
||||
); */
|
||||
let initial =
|
||||
if before != Marker::NoChildren && (initial == None || initial == Some(Child::Null)) {
|
||||
Some(Child::Nodes(vec![]))
|
||||
} else {
|
||||
initial
|
||||
};
|
||||
|
||||
match value {
|
||||
Child::Fn(f) => {
|
||||
|
@ -148,6 +154,7 @@ pub fn insert(
|
|||
}
|
||||
|
||||
Some(insert_expression(
|
||||
cx,
|
||||
parent.clone().unchecked_into(),
|
||||
&value,
|
||||
current,
|
||||
|
@ -160,6 +167,7 @@ pub fn insert(
|
|||
}
|
||||
_ => {
|
||||
insert_expression(
|
||||
cx,
|
||||
parent.unchecked_into(),
|
||||
&value,
|
||||
initial.unwrap_or(Child::Null),
|
||||
|
@ -170,11 +178,19 @@ pub fn insert(
|
|||
}
|
||||
|
||||
pub fn insert_expression(
|
||||
cx: Scope,
|
||||
parent: web_sys::Element,
|
||||
new_value: &Child,
|
||||
mut current: Child,
|
||||
before: &Marker,
|
||||
) -> Child {
|
||||
#[cfg(feature = "hydrate")]
|
||||
if cx.is_hydrating() && current == Child::Null {
|
||||
current = Child::Nodes(child_nodes(&parent));
|
||||
}
|
||||
|
||||
log::debug!("insert_expression\nparent = {}\nnew_value = {new_value:?}\ncurrent = {current:?}\nbefore = {before:?}", parent.node_name());
|
||||
|
||||
if new_value == ¤t {
|
||||
current
|
||||
} else {
|
||||
|
@ -211,7 +227,11 @@ pub fn insert_expression(
|
|||
}
|
||||
Child::Null => match before {
|
||||
Marker::BeforeChild(before) => {
|
||||
Child::Node(insert_before(&parent, node, Some(before)))
|
||||
if before.is_connected() {
|
||||
Child::Node(insert_before(&parent, node, Some(before)))
|
||||
} else {
|
||||
Child::Node(append_child(&parent, node))
|
||||
}
|
||||
}
|
||||
_ => Child::Node(append_child(&parent, node)),
|
||||
},
|
||||
|
@ -387,3 +407,14 @@ fn clean_children(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn child_nodes(parent: &web_sys::Element) -> Vec<web_sys::Node> {
|
||||
let children = parent.children();
|
||||
let mut nodes = Vec::new();
|
||||
for idx in 0..children.length() {
|
||||
if let Some(node) = children.item(idx) {
|
||||
nodes.push(node.clone().unchecked_into());
|
||||
}
|
||||
}
|
||||
nodes
|
||||
}
|
||||
|
|
|
@ -103,6 +103,7 @@ fn root_element_to_tokens(template_uid: &Ident, node: &Node, mode: Mode) -> Toke
|
|||
// for hydration, use get_next_element(), which will either draw from an SSRed node or clone the template
|
||||
Mode::Hydrate => quote! {
|
||||
let root = #template_uid.with(|template| cx.get_next_element(template));
|
||||
log::debug!("root node = {:#?}", root.outer_html());
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -207,21 +208,21 @@ fn element_to_tokens(
|
|||
quote_spanned! {
|
||||
span => //let #this_el_ident = #debug_name;
|
||||
let #this_el_ident = #parent.clone().unchecked_into::<web_sys::Node>();
|
||||
//log::debug!("=> got {}", #this_el_ident.node_name());
|
||||
log::debug!("=> got {}", #this_el_ident.node_name());
|
||||
}
|
||||
} else if let Some(prev_sib) = &prev_sib {
|
||||
quote_spanned! {
|
||||
span => //let #this_el_ident = #debug_name;
|
||||
//log::debug!("next_sibling ({})", #debug_name);
|
||||
log::debug!("next_sibling ({})", #debug_name);
|
||||
let #this_el_ident = #prev_sib.next_sibling().unwrap_throw();
|
||||
//log::debug!("=> got {}", #this_el_ident.node_name());
|
||||
log::debug!("=> got {}", #this_el_ident.node_name());
|
||||
}
|
||||
} else {
|
||||
quote_spanned! {
|
||||
span => //let #this_el_ident = #debug_name;
|
||||
//log::debug!("first_child ({})", #debug_name);
|
||||
log::debug!("first_child ({})", #debug_name);
|
||||
let #this_el_ident = #parent.first_child().unwrap_throw();
|
||||
//log::debug!("=> got {}", #this_el_ident.node_name());
|
||||
log::debug!("=> got {}", #this_el_ident.node_name());
|
||||
}
|
||||
};
|
||||
navigations.push(this_nav);
|
||||
|
@ -465,7 +466,7 @@ fn attr_to_tokens(
|
|||
(AttributeValue::Dynamic(value), Mode::Ssr) => {
|
||||
expressions.push(quote_spanned! {
|
||||
span => leptos_buffer.push(' ');
|
||||
leptos_buffer.push_str(&#value.into_attribute(cx).as_value_string(#name));
|
||||
leptos_buffer.push_str(&{#value}.into_attribute(cx).as_value_string(#name));
|
||||
});
|
||||
}
|
||||
(AttributeValue::Dynamic(value), _) => {
|
||||
|
@ -508,6 +509,9 @@ fn child_to_tokens(
|
|||
next_sib,
|
||||
template,
|
||||
expressions,
|
||||
navigations,
|
||||
next_el_id,
|
||||
next_co_id,
|
||||
multi,
|
||||
mode,
|
||||
)
|
||||
|
@ -605,7 +609,8 @@ fn child_to_tokens(
|
|||
template.push_str("<!#><!/>");
|
||||
navigations.push(quote! {
|
||||
#location;
|
||||
let (#el, #co) = cx.get_next_marker(&#name);
|
||||
let (#el, #co) = cx.get_next_marker(&#parent);
|
||||
log::debug!("get_next_marker => {} [{:?}]", #el.node_name(), #co.iter().map(|c| c.node_name()).collect::<Vec<_>>());
|
||||
});
|
||||
|
||||
current = Some(co);
|
||||
|
@ -645,9 +650,12 @@ fn child_to_tokens(
|
|||
fn component_to_tokens(
|
||||
node: &Node,
|
||||
parent: Option<&Ident>,
|
||||
next_sib: Option<Ident>,
|
||||
mut next_sib: Option<Ident>,
|
||||
template: &mut String,
|
||||
expressions: &mut Vec<TokenStream>,
|
||||
navigations: &mut Vec<TokenStream>,
|
||||
next_el_id: &mut usize,
|
||||
next_co_id: &mut usize,
|
||||
multi: bool,
|
||||
mode: Mode,
|
||||
) -> PrevSibChange {
|
||||
|
@ -669,11 +677,37 @@ fn component_to_tokens(
|
|||
if mode == Mode::Ssr {
|
||||
expressions.push(quote::quote_spanned! {
|
||||
span => // TODO wrap components but use get_next_element() instead of first_child/next_sibling?
|
||||
//leptos_buffer.push_str("<!--#-->");
|
||||
leptos_buffer.push_str("<!--#-->");
|
||||
leptos_buffer.push_str(&#create_component.into_child(cx).as_child_string());
|
||||
//leptos_buffer.push_str("<!--/-->");
|
||||
leptos_buffer.push_str("<!--/-->");
|
||||
|
||||
});
|
||||
} else if mode == Mode::Hydrate {
|
||||
let name = child_ident(*next_el_id, node);
|
||||
*next_el_id += 1;
|
||||
let el = child_ident(*next_el_id, node);
|
||||
*next_co_id += 1;
|
||||
let co = comment_ident(*next_co_id, node);
|
||||
next_sib = Some(el.clone());
|
||||
|
||||
template.push_str("<!#><!/>");
|
||||
navigations.push(quote! {
|
||||
let (#el, #co) = cx.get_next_marker(&#name);
|
||||
log::debug!("component get_next_marker => {} [{:?}]", #el.node_name(), #co.iter().map(|c| c.node_name()).collect::<Vec<_>>());
|
||||
});
|
||||
|
||||
//current = Some(co);
|
||||
|
||||
expressions.push(quote! {
|
||||
log::debug!("inserting! really!");
|
||||
leptos::insert(
|
||||
cx,
|
||||
#el,
|
||||
#create_component.into_child(cx),
|
||||
Marker::NoChildren,
|
||||
Some(Child::Nodes(#co)),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
expressions.push(quote! {
|
||||
leptos::insert(
|
||||
|
@ -705,7 +739,7 @@ fn create_component(node: &Node, mode: Mode) -> TokenStream {
|
|||
if mode == Mode::Hydrate {
|
||||
(
|
||||
quote_spanned! { span => let children = vec![#child]; },
|
||||
quote_spanned! { span => .children(Box::new(move || children)) },
|
||||
quote_spanned! { span => .children(Box::new(move || children.clone())) },
|
||||
)
|
||||
} else {
|
||||
(
|
||||
|
|
|
@ -100,7 +100,7 @@ impl SharedContext {
|
|||
completed: Default::default(),
|
||||
events: Default::default(),
|
||||
context: Some(HydrationContext {
|
||||
id: "0-".into(),
|
||||
id: "".into(),
|
||||
count: -1,
|
||||
}),
|
||||
registry,
|
||||
|
@ -111,8 +111,9 @@ impl SharedContext {
|
|||
|
||||
pub fn next_hydration_key(&mut self) -> String {
|
||||
if let Some(context) = &mut self.context {
|
||||
let k = format!("{}{}", context.id, context.count);
|
||||
context.count += 1;
|
||||
format!("{}{}", context.id, context.count)
|
||||
k
|
||||
} else {
|
||||
self.context = Some(HydrationContext {
|
||||
id: "0-".into(),
|
||||
|
|
|
@ -124,6 +124,11 @@ impl Scope {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub fn is_hydrating(&self) -> bool {
|
||||
self.runtime.shared_context.borrow().is_some()
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub fn start_hydration(&self, element: &web_sys::Element) {
|
||||
self.runtime.start_hydration(element);
|
||||
|
|
|
@ -10,6 +10,7 @@ log = "0.4"
|
|||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"HtmlLinkElement",
|
||||
"HtmlTitleElement"
|
||||
]
|
||||
|
||||
|
|
102
meta/src/lib.rs
102
meta/src/lib.rs
|
@ -2,9 +2,15 @@ use std::{cell::RefCell, fmt::Debug, rc::Rc};
|
|||
|
||||
use leptos::*;
|
||||
|
||||
mod stylesheet;
|
||||
mod title;
|
||||
pub use stylesheet::*;
|
||||
pub use title::*;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MetaContext {
|
||||
title: TitleContext,
|
||||
pub(crate) title: TitleContext,
|
||||
pub(crate) stylesheets: StylesheetContext,
|
||||
}
|
||||
|
||||
pub fn use_head(cx: Scope) -> MetaContext {
|
||||
|
@ -21,18 +27,22 @@ impl MetaContext {
|
|||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct TitleContext {
|
||||
el: Rc<RefCell<Option<web_sys::HtmlTitleElement>>>,
|
||||
formatter: Rc<RefCell<Option<Formatter>>>,
|
||||
text: Rc<RefCell<Option<TextProp>>>,
|
||||
}
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn dehydrate(&self) -> String {
|
||||
let mut tags = String::new();
|
||||
|
||||
impl Debug for TitleContext {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_tuple("TitleContext").finish()
|
||||
// Title
|
||||
if let Some(title) = self.title.as_string() {
|
||||
tags.push_str("<title>");
|
||||
tags.push_str(&title);
|
||||
tags.push_str("</title>");
|
||||
}
|
||||
|
||||
// Stylesheets
|
||||
tags.push_str(&self.stylesheets.as_string());
|
||||
|
||||
tags
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,73 +75,3 @@ where
|
|||
TextProp(Box::new(s))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Formatter(Box<dyn Fn(String) -> String>);
|
||||
|
||||
impl<F> From<F> for Formatter
|
||||
where
|
||||
F: Fn(String) -> String + 'static,
|
||||
{
|
||||
fn from(f: F) -> Formatter {
|
||||
Formatter(Box::new(f))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[component]
|
||||
pub fn Title(cx: Scope, formatter: Option<Formatter>, text: Option<TextProp>) {
|
||||
let meta = use_head(cx);
|
||||
if let Some(formatter) = formatter {
|
||||
*meta.title.formatter.borrow_mut() = Some(formatter);
|
||||
}
|
||||
if let Some(text) = text {
|
||||
*meta.title.text.borrow_mut() = Some(text.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
#[component]
|
||||
pub fn Title(cx: Scope, formatter: Option<Formatter>, text: Option<TextProp>) {
|
||||
let meta = use_head(cx);
|
||||
if let Some(formatter) = formatter {
|
||||
*meta.title.formatter.borrow_mut() = Some(formatter);
|
||||
}
|
||||
if let Some(text) = text {
|
||||
*meta.title.text.borrow_mut() = Some(text.into());
|
||||
}
|
||||
|
||||
let el_ref = meta.title.el.borrow_mut();
|
||||
let el = if let Some(el) = &*el_ref {
|
||||
el.clone()
|
||||
} else {
|
||||
match document().query_selector("title") {
|
||||
Ok(Some(title)) => title.unchecked_into(),
|
||||
_ => {
|
||||
let el = document().create_element("title").unwrap_throw();
|
||||
document()
|
||||
.query_selector("head")
|
||||
.unwrap_throw()
|
||||
.unwrap_throw()
|
||||
.append_child(el.unchecked_ref())
|
||||
.unwrap_throw();
|
||||
el.unchecked_into()
|
||||
}
|
||||
}
|
||||
};
|
||||
create_render_effect(cx, move |_| {
|
||||
let text = meta
|
||||
.title
|
||||
.text
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.map(|f| (f.0)())
|
||||
.unwrap_or_default();
|
||||
let text = if let Some(formatter) = &*meta.title.formatter.borrow() {
|
||||
(formatter.0)(text)
|
||||
} else {
|
||||
text
|
||||
};
|
||||
|
||||
el.set_text_content(Some(&text));
|
||||
});
|
||||
}
|
||||
|
|
52
meta/src/stylesheet.rs
Normal file
52
meta/src/stylesheet.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
use crate::{use_head, MetaContext};
|
||||
use leptos::*;
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct StylesheetContext {
|
||||
els: Rc<RefCell<HashMap<String, Option<web_sys::HtmlLinkElement>>>>,
|
||||
}
|
||||
|
||||
impl StylesheetContext {
|
||||
pub fn as_string(&self) -> String {
|
||||
self.els
|
||||
.borrow()
|
||||
.iter()
|
||||
.map(|(href, _)| format!(r#"<link rel="stylesheet" href="{href}">"#))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[component]
|
||||
pub fn Stylesheet(cx: Scope, href: String) {
|
||||
let meta = use_head(cx);
|
||||
meta.stylesheets.els.borrow_mut().insert(href, None);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
#[component]
|
||||
pub fn Stylesheet(cx: Scope, href: String) {
|
||||
use leptos::document;
|
||||
|
||||
let meta = use_head(cx);
|
||||
let existing_el = meta.stylesheets.els.borrow();
|
||||
let existing_el = existing_el.get(&href).clone();
|
||||
if let Some(Some(_)) = existing_el {
|
||||
log::warn!("<Stylesheet/> already loaded stylesheet {href}");
|
||||
} else {
|
||||
let el = document().create_element("link").unwrap_throw();
|
||||
el.set_attribute("rel", "stylesheet").unwrap_throw();
|
||||
el.set_attribute("href", &href).unwrap_throw();
|
||||
document()
|
||||
.query_selector("head")
|
||||
.unwrap_throw()
|
||||
.unwrap_throw()
|
||||
.append_child(el.unchecked_ref())
|
||||
.unwrap_throw();
|
||||
meta.stylesheets
|
||||
.els
|
||||
.borrow_mut()
|
||||
.insert(href, Some(el.unchecked_into()));
|
||||
}
|
||||
}
|
95
meta/src/title.rs
Normal file
95
meta/src/title.rs
Normal file
|
@ -0,0 +1,95 @@
|
|||
use crate::{use_head, MetaContext, TextProp};
|
||||
use leptos::*;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct TitleContext {
|
||||
el: Rc<RefCell<Option<web_sys::HtmlTitleElement>>>,
|
||||
formatter: Rc<RefCell<Option<Formatter>>>,
|
||||
text: Rc<RefCell<Option<TextProp>>>,
|
||||
}
|
||||
|
||||
impl TitleContext {
|
||||
pub fn as_string(&self) -> Option<String> {
|
||||
let title = self.text.borrow().as_ref().map(|f| (f.0)());
|
||||
title.map(|title| {
|
||||
if let Some(formatter) = &*self.formatter.borrow() {
|
||||
(formatter.0)(title)
|
||||
} else {
|
||||
title
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for TitleContext {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_tuple("TitleContext").finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Formatter(Box<dyn Fn(String) -> String>);
|
||||
|
||||
impl<F> From<F> for Formatter
|
||||
where
|
||||
F: Fn(String) -> String + 'static,
|
||||
{
|
||||
fn from(f: F) -> Formatter {
|
||||
Formatter(Box::new(f))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[component]
|
||||
pub fn Title(cx: Scope, formatter: Option<Formatter>, text: Option<TextProp>) {
|
||||
let meta = use_head(cx);
|
||||
if let Some(formatter) = formatter {
|
||||
*meta.title.formatter.borrow_mut() = Some(formatter);
|
||||
}
|
||||
if let Some(text) = text {
|
||||
*meta.title.text.borrow_mut() = Some(text.into());
|
||||
}
|
||||
log::debug!("setting title to {:?}", meta.title.as_string());
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
#[component]
|
||||
pub fn Title(cx: Scope, formatter: Option<Formatter>, text: Option<TextProp>) {
|
||||
use crate::use_head;
|
||||
|
||||
let meta = use_head(cx);
|
||||
if let Some(formatter) = formatter {
|
||||
*meta.title.formatter.borrow_mut() = Some(formatter);
|
||||
}
|
||||
if let Some(text) = text {
|
||||
*meta.title.text.borrow_mut() = Some(text.into());
|
||||
}
|
||||
|
||||
let el = {
|
||||
let el_ref = meta.title.el.borrow_mut();
|
||||
let el = if let Some(el) = &*el_ref {
|
||||
el.clone()
|
||||
} else {
|
||||
match document().query_selector("title") {
|
||||
Ok(Some(title)) => title.unchecked_into(),
|
||||
_ => {
|
||||
let el = document().create_element("title").unwrap_throw();
|
||||
document()
|
||||
.query_selector("head")
|
||||
.unwrap_throw()
|
||||
.unwrap_throw()
|
||||
.append_child(el.unchecked_ref())
|
||||
.unwrap_throw();
|
||||
el.unchecked_into()
|
||||
}
|
||||
}
|
||||
};
|
||||
el
|
||||
};
|
||||
|
||||
create_render_effect(cx, move |_| {
|
||||
let text = meta.title.as_string().unwrap_or_default();
|
||||
|
||||
el.set_text_content(Some(&text));
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue