This commit is contained in:
Greg Johnston 2023-01-05 11:08:11 -05:00
commit 9d8627b337
7 changed files with 213 additions and 149 deletions

View File

@ -155,7 +155,13 @@ impl Scope {
// Internals
impl Scope {
pub(crate) fn dispose(self) {
/// Disposes of this reactive scope.
///
/// This will
/// 1. dispose of all child `Scope`s
/// 2. run all cleanup functions defined for this scope by [on_cleanup](crate::on_cleanup).
/// 3. dispose of all signals, effects, and resources owned by this `Scope`.
pub fn dispose(self) {
with_runtime(self.runtime, |runtime| {
// dispose of all child scopes
let children = {
@ -282,9 +288,9 @@ impl Scope {
with_runtime(self.runtime, |runtime| runtime.all_resources())
}
/// Returns IDs for all [Resource](crate::Resource)s found on any scope that are
/// pending from the server.
pub fn pending_resources(&self) -> Vec<ResourceId> {
/// Returns IDs for all [Resource](crate::Resource)s found on any scope that are
/// pending from the server.
pub fn pending_resources(&self) -> Vec<ResourceId> {
with_runtime(self.runtime, |runtime| runtime.pending_resources())
}
@ -323,7 +329,7 @@ impl Scope {
Box::pin(async move {
rx.next().await;
resolver()
})
}),
),
);
})
@ -344,4 +350,4 @@ impl fmt::Debug for ScopeDisposer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("ScopeDisposer").finish()
}
}
}

View File

@ -781,10 +781,18 @@ impl SignalId {
pub(crate) fn subscribe(&self, runtime: &Runtime) {
// add subscriber
if let Some(observer) = runtime.observer.get() {
// add this observer to the signal's dependencies (to allow notification)
let mut subs = runtime.signal_subscribers.borrow_mut();
if let Some(subs) = subs.entry(*self) {
subs.or_default().borrow_mut().insert(observer);
}
// add this signal to the effect's sources (to allow cleanup)
let mut effect_sources = runtime.effect_sources.borrow_mut();
if let Some(effect_sources) = effect_sources.entry(observer) {
let sources = effect_sources.or_default();
sources.borrow_mut().insert(*self);
}
}
}

View File

@ -1,14 +1,18 @@
use cfg_if::cfg_if;
use leptos::{Scope, component, IntoView};
use std::{rc::Rc, cell::{RefCell, Cell}, collections::HashMap};
use leptos::{component, IntoView, Scope};
use std::{
cell::{Cell, RefCell},
collections::HashMap,
rc::Rc,
};
use crate::{use_head, TextProp};
/// Manages all of the `<meta>` elements set by [Meta] components.
#[derive(Clone, Default, Debug)]
pub struct MetaTagsContext {
next_id: Cell<MetaTagId>,
#[allow(clippy::type_complexity)]
next_id: Cell<MetaTagId>,
#[allow(clippy::type_complexity)]
els: Rc<RefCell<HashMap<MetaTagId, (Option<MetaTag>, Option<web_sys::HtmlMetaElement>)>>>,
}
@ -16,25 +20,25 @@ pub struct MetaTagsContext {
struct MetaTagId(usize);
impl MetaTagsContext {
fn get_next_id(&self) -> MetaTagId {
let current_id = self.next_id.get();
let next_id = MetaTagId(current_id.0 + 1);
self.next_id.set(next_id);
next_id
}
fn get_next_id(&self) -> MetaTagId {
let current_id = self.next_id.get();
let next_id = MetaTagId(current_id.0 + 1);
self.next_id.set(next_id);
next_id
}
}
#[derive(Clone, Debug)]
enum MetaTag {
Charset(TextProp),
HttpEquiv {
http_equiv: TextProp,
content: Option<TextProp>
},
Name {
name: TextProp,
content: TextProp
}
Charset(TextProp),
HttpEquiv {
http_equiv: TextProp,
content: Option<TextProp>,
},
Name {
name: TextProp,
content: TextProp,
},
}
impl MetaTagsContext {
@ -86,22 +90,21 @@ impl MetaTagsContext {
/// ```
#[component(transparent)]
pub fn Meta(
cx: Scope,
cx: Scope,
/// The [`charset`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-charset) attribute.
#[prop(optional, into)]
charset: Option<TextProp>,
/// The [`name`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-name) attribute.
#[prop(optional, into)]
name: Option<TextProp>,
/// The [`http-equiv`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-http-equiv) attribute.
#[prop(optional, into)]
http_equiv: Option<TextProp>,
/// The [`content`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-content) attribute.
#[prop(optional, into)]
content: Option<TextProp>
/// The [`name`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-name) attribute.
#[prop(optional, into)]
name: Option<TextProp>,
/// The [`http-equiv`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-http-equiv) attribute.
#[prop(optional, into)]
http_equiv: Option<TextProp>,
/// The [`content`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-content) attribute.
#[prop(optional, into)]
content: Option<TextProp>,
) -> impl IntoView {
let tag = match (charset, name, http_equiv, content) {
let tag = match (charset, name, http_equiv, content) {
(Some(charset), _, _, _) => MetaTag::Charset(charset),
(_, _, Some(http_equiv), content) => MetaTag::HttpEquiv { http_equiv, content },
(_, Some(name), _, Some(content)) => MetaTag::Name { name, content },
@ -113,69 +116,76 @@ pub fn Meta(
use leptos::{document, JsCast, UnwrapThrowExt, create_effect};
let meta = use_head(cx);
let meta_tags = meta.meta_tags;
let id = meta_tags.get_next_id();
let meta_tags = meta.meta_tags;
let id = meta_tags.get_next_id();
let el = if let Ok(Some(el)) = document().query_selector(&format!("[data-leptos-meta='{}']", id.0)) {
el
} else {
document().create_element("meta").unwrap_throw()
};
let el = if let Ok(Some(el)) = document().query_selector(&format!("[data-leptos-meta='{}']", id.0)) {
el
} else {
document().create_element("meta").unwrap_throw()
};
match tag {
MetaTag::Charset(charset) => {
create_effect(cx, {
let el = el.clone();
move |_| {
_ = el.set_attribute("charset", &charset.get());
}
})
},
MetaTag::HttpEquiv { http_equiv, content } => {
create_effect(cx, {
let el = el.clone();
move |_| {
_ = el.set_attribute("http-equiv", &http_equiv.get());
}
});
if let Some(content) = content {
create_effect(cx, {
let el = el.clone();
move |_| {
_ = el.set_attribute("content", &content.get());
}
});
}
},
MetaTag::Name { name, content } => {
create_effect(cx, {
let el = el.clone();
move |_| {
_ = el.set_attribute("name", &name.get());
}
});
create_effect(cx, {
let el = el.clone();
move |_| {
_ = el.set_attribute("content", &content.get());
}
});
},
}
match tag {
MetaTag::Charset(charset) => {
create_effect(cx, {
let el = el.clone();
move |_| {
_ = el.set_attribute("charset", &charset.get());
}
})
},
MetaTag::HttpEquiv { http_equiv, content } => {
create_effect(cx, {
let el = el.clone();
move |_| {
_ = el.set_attribute("http-equiv", &http_equiv.get());
}
});
if let Some(content) = content {
create_effect(cx, {
let el = el.clone();
move |_| {
_ = el.set_attribute("content", &content.get());
}
});
}
},
MetaTag::Name { name, content } => {
create_effect(cx, {
let el = el.clone();
move |_| {
_ = el.set_attribute("name", &name.get());
}
});
create_effect(cx, {
let el = el.clone();
move |_| {
_ = el.set_attribute("content", &content.get());
}
});
},
}
// add to head
document()
.query_selector("head")
.unwrap_throw()
.unwrap_throw()
.append_child(&el)
.unwrap_throw();
// add to head
let head = document()
.query_selector("head")
.unwrap_throw()
.unwrap_throw();
head.append_child(&el)
.unwrap_throw();
// add to meta tags
meta_tags.els.borrow_mut().insert(id, (None, Some(el.unchecked_into())));
leptos::on_cleanup(cx, {
let el = el.clone();
move || {
head.remove_child(&el);
}
});
// add to meta tags
meta_tags.els.borrow_mut().insert(id, (None, Some(el.unchecked_into())));
} else {
let meta = use_head(cx);
let meta_tags = meta.meta_tags;
let meta_tags = meta.meta_tags;
meta_tags.els.borrow_mut().insert(meta_tags.get_next_id(), (Some(tag), None));
}
}

View File

@ -1,13 +1,37 @@
use crate::use_head;
use cfg_if::cfg_if;
use leptos::*;
use std::{cell::RefCell, collections::HashMap, rc::Rc};
use std::{
cell::{Cell, RefCell},
collections::HashMap,
rc::Rc,
};
/// Manages all of the stylesheets set by [Stylesheet] components.
#[derive(Clone, Default, Debug)]
pub struct StylesheetContext {
#[allow(clippy::type_complexity)]
els: Rc<RefCell<HashMap<(Option<String>, String), Option<web_sys::HtmlLinkElement>>>>,
// key is (id, href)
els: Rc<RefCell<HashMap<StyleSheetData, Option<web_sys::HtmlLinkElement>>>>,
next_id: Rc<Cell<StylesheetId>>,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
struct StylesheetId(usize);
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct StyleSheetData {
id: String,
href: String,
}
impl StylesheetContext {
fn get_next_id(&self) -> StylesheetId {
let current_id = self.next_id.get();
let next_id = StylesheetId(current_id.0 + 1);
self.next_id.set(next_id);
next_id
}
}
impl StylesheetContext {
@ -16,12 +40,8 @@ impl StylesheetContext {
self.els
.borrow()
.iter()
.map(|((id, href), _)| {
if let Some(id) = id {
format!(r#"<link rel="stylesheet" id="{id}" href="{href}">"#)
} else {
format!(r#"<link rel="stylesheet" href="{href}">"#)
}
.map(|(StyleSheetData { id, href }, _)| {
format!(r#"<link rel="stylesheet" id="{id}" href="{href}">"#)
})
.collect()
}
@ -55,50 +75,50 @@ pub fn Stylesheet(
#[prop(optional, into)]
id: Option<String>,
) -> impl IntoView {
let meta = use_head(cx);
let stylesheets = &meta.stylesheets;
let next_id = stylesheets.get_next_id();
let id = id.unwrap_or_else(|| format!("leptos-style-{}", next_id.0));
let key = StyleSheetData { id, href };
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
use leptos::document;
let meta = use_head(cx);
let element_to_hydrate = document().get_element_by_id(&key.id);
// TODO I guess this will create a duplicated <link> when hydrating
let existing_el = {
let els = meta.stylesheets.els.borrow();
let key = (id.clone(), href.clone());
els.get(&key).cloned()
};
if let Some(Some(_)) = existing_el {
leptos::leptos_dom::debug_warn!("<Stylesheet/> already loaded stylesheet {href}");
} else {
let element_to_hydrate = id.as_ref()
.and_then(|id| {
document().get_element_by_id(&id)
});
let el = element_to_hydrate.unwrap_or_else(|| {
let el = document().create_element("link").unwrap_throw();
el.set_attribute("rel", "stylesheet").unwrap_throw();
el.set_attribute("id", &key.id).unwrap_throw();
el.set_attribute("href", &key.href).unwrap_throw();
let head = document().head().unwrap_throw();
head
.append_child(el.unchecked_ref())
.unwrap_throw();
let el = element_to_hydrate.unwrap_or_else(|| {
let el = document().create_element("link").unwrap_throw();
el.set_attribute("rel", "stylesheet").unwrap_throw();
if let Some(id_val) = &id{
el.set_attribute("id", id_val).unwrap_throw();
}
el.set_attribute("href", &href).unwrap_throw();
document()
.query_selector("head")
.unwrap_throw()
.unwrap_throw()
.append_child(el.unchecked_ref())
.unwrap_throw();
el
});
el
});
on_cleanup(cx, {
let el = el.clone();
let els = meta.stylesheets.els.clone();
let key = key.clone();
move || {
let head = document().head().unwrap_throw();
_ = head.remove_child(&el);
els.borrow_mut().remove(&key);
}
});
meta.stylesheets
.els
.borrow_mut()
.insert(key, Some(el.unchecked_into()));
meta.stylesheets
.els
.borrow_mut()
.insert((id, href), Some(el.unchecked_into()));
}
} else {
let meta = use_head(cx);
meta.stylesheets.els.borrow_mut().insert((id,href), None);
meta.stylesheets.els.borrow_mut().insert(key, None);
}
}
}

View File

@ -106,24 +106,40 @@ pub fn Title(
}
let el = {
let el_ref = meta.title.el.borrow_mut();
let mut el_ref = meta.title.el.borrow_mut();
let el = if let Some(el) = &*el_ref {
let prev_text = el.inner_text();
on_cleanup(cx, {
let el = el.clone();
move || {
_ = el.set_text(&prev_text);
}
});
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())
let head = document().head().unwrap_throw();
head.append_child(el.unchecked_ref())
.unwrap_throw();
on_cleanup(cx, {
let el = el.clone();
move || {
_ = head.remove_child(&el);
}
});
el.unchecked_into()
}
}
};
*el_ref = Some(el.clone().unchecked_into());
el
};

View File

@ -8,18 +8,21 @@ use leptos::*;
#[component]
pub fn Outlet(cx: Scope) -> impl IntoView {
let route = use_route(cx);
let is_showing = Rc::new(Cell::new(None));
let is_showing = Rc::new(Cell::new(None::<(usize, Scope)>));
let (outlet, set_outlet) = create_signal(cx, None);
create_effect(cx, move |_| {
create_isomorphic_effect(cx, move |_| {
match (route.child(), &is_showing.get()) {
(None, _) => {
set_outlet.set(None);
}
(Some(child), Some(is_showing_val)) if child.id() == *is_showing_val => {
(Some(child), Some((is_showing_val, _))) if child.id() == *is_showing_val => {
// do nothing: we don't need to rerender the component, because it's the same
}
(Some(child), _) => {
is_showing.set(Some(child.id()));
(Some(child), prev) => {
if let Some(prev_scope) = prev.map(|(_, scope)| scope) {
prev_scope.dispose();
}
is_showing.set(Some((child.id(), child.cx())));
provide_context(child.cx(), child.clone());
set_outlet.set(Some(child.outlet().into_view(cx)))
}

View File

@ -140,7 +140,8 @@ pub fn Routes(
if disposers.borrow().len() > i + 1 {
let mut disposers = disposers.borrow_mut();
let old_route_disposer = std::mem::replace(&mut disposers[i], disposer);
let old_route_disposer =
std::mem::replace(&mut disposers[i + 1], disposer);
old_route_disposer.dispose();
} else {
disposers.borrow_mut().push(disposer);