use std::cell::RefCell;
use std::collections::HashMap;
use std::marker::PhantomData;
use wasm_bindgen::JsCast;
use web_sys::{Document, Element, HtmlElement, Window};
use crate::handle::Handle;
use crate::reactive::ScopeId;
use crate::scope::Scope;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum LifecyclePhase {
Setup,
Mount,
Ready,
Unmount,
}
#[non_exhaustive]
#[derive(Clone, Copy)]
pub struct LifecycleContext<'a> {
pub el: &'a Element,
pub scope_id: ScopeId,
pub phase: LifecyclePhase,
}
impl<'a> LifecycleContext<'a> {
#[doc(hidden)]
pub fn __new(el: &'a Element, scope_id: ScopeId, phase: LifecyclePhase) -> Self {
Self {
el,
scope_id,
phase,
}
}
}
#[track_caller]
fn check_phase(ctx_phase: LifecyclePhase, allowed: &[LifecyclePhase], extractor: &str) {
if !allowed.contains(&ctx_phase) {
panic!(
"{extractor} extractor is not valid in `on_{phase}` (allowed: {allowed:?}). \
At setup the rendered template hasn't been walked yet; at unmount the element \
may already be detaching. Reach for Handle / Inject / Parent / NearestParent \
/ ScopeId / Doc / Win / Body — those work in every phase.",
phase = match ctx_phase {
LifecyclePhase::Setup => "setup",
LifecyclePhase::Mount => "mount",
LifecyclePhase::Ready => "ready",
LifecyclePhase::Unmount => "unmount",
},
);
}
}
const ELEMENT_PHASES: &[LifecyclePhase] = &[LifecyclePhase::Mount, LifecyclePhase::Ready];
#[derive(Clone, Copy)]
pub struct El<'a>(pub &'a Element);
impl<'a> std::ops::Deref for El<'a> {
type Target = Element;
fn deref(&self) -> &Self::Target {
self.0
}
}
impl<'a> From<LifecycleContext<'a>> for El<'a> {
#[track_caller]
fn from(ctx: LifecycleContext<'a>) -> Self {
check_phase(ctx.phase, ELEMENT_PHASES, "El");
El(ctx.el)
}
}
impl<'a> From<LifecycleContext<'a>> for ScopeId {
fn from(ctx: LifecycleContext<'a>) -> Self {
ctx.scope_id
}
}
impl<'a, T: 'static> From<LifecycleContext<'a>> for Handle<T> {
fn from(ctx: LifecycleContext<'a>) -> Self {
let scope = Scope::find(ctx.scope_id)
.expect("LifecycleContext carried a scope id whose entry no longer exists");
let rc = scope
.typed::<T>()
.expect("Handle<T>: `T` doesn't match this scope's Rust type");
Handle::new(rc, ctx.scope_id)
}
}
#[derive(Clone, Copy)]
pub struct ParentId(pub Option<ScopeId>);
impl<'a> From<LifecycleContext<'a>> for ParentId {
fn from(ctx: LifecycleContext<'a>) -> Self {
ParentId(crate::context::parent_of(ctx.scope_id))
}
}
#[derive(Clone)]
pub struct Doc(pub Document);
impl<'a> From<LifecycleContext<'a>> for Doc {
fn from(_: LifecycleContext<'a>) -> Self {
Doc(web_sys::window()
.and_then(|w| w.document())
.expect("Doc extractor: no document"))
}
}
#[derive(Clone)]
pub struct Win(pub Window);
impl<'a> From<LifecycleContext<'a>> for Win {
fn from(_: LifecycleContext<'a>) -> Self {
Win(web_sys::window().expect("Win extractor: no window"))
}
}
#[derive(Clone)]
pub struct Body(pub HtmlElement);
impl<'a> From<LifecycleContext<'a>> for Body {
fn from(_: LifecycleContext<'a>) -> Self {
Body(
web_sys::window()
.and_then(|w| w.document())
.and_then(|d| d.body())
.expect("Body extractor: no document.body"),
)
}
}
#[derive(Clone, Copy)]
pub struct TagName(pub &'static str);
impl<'a> From<LifecycleContext<'a>> for TagName {
#[track_caller]
fn from(ctx: LifecycleContext<'a>) -> Self {
check_phase(ctx.phase, ELEMENT_PHASES, "TagName");
let name = ctx
.el
.parent_element()
.map(|p| p.tag_name().to_lowercase())
.unwrap_or_else(|| ctx.el.tag_name().to_lowercase());
TagName(Box::leak(name.into_boxed_str()))
}
}
#[derive(Clone, Copy)]
pub struct Refs<'a> {
scope_id: ScopeId,
_m: PhantomData<&'a ()>,
}
impl<'a> Refs<'a> {
pub fn get(&self, name: &str) -> Option<Element> {
crate::refs::get_on(self.scope_id, name)
}
pub fn get_as<T: JsCast>(&self, name: &str) -> Option<T> {
self.get(name).and_then(|el| el.dyn_into().ok())
}
pub fn get_component<T: 'static>(&self, name: &str) -> Option<crate::handle::Handle<T>> {
crate::refs::get_component_on::<T>(self.scope_id, name)
}
}
impl<'a> From<LifecycleContext<'a>> for Refs<'a> {
#[track_caller]
fn from(ctx: LifecycleContext<'a>) -> Self {
check_phase(ctx.phase, ELEMENT_PHASES, "Refs");
Refs {
scope_id: ctx.scope_id,
_m: PhantomData,
}
}
}
pub struct TypedEl<T: JsCast>(pub T);
impl<'a, T: JsCast + 'static> From<LifecycleContext<'a>> for TypedEl<T> {
#[track_caller]
fn from(ctx: LifecycleContext<'a>) -> Self {
check_phase(ctx.phase, ELEMENT_PHASES, "TypedEl");
TypedEl(
ctx.el
.clone()
.dyn_into::<T>()
.expect("TypedEl<T>: rendered root doesn't cast to T"),
)
}
}
impl<'a, T: JsCast + 'static> From<LifecycleContext<'a>> for Option<TypedEl<T>> {
#[track_caller]
fn from(ctx: LifecycleContext<'a>) -> Self {
check_phase(ctx.phase, ELEMENT_PHASES, "Option<TypedEl>");
ctx.el.clone().dyn_into::<T>().ok().map(TypedEl)
}
}
#[derive(Clone)]
pub struct HostEl(pub Element);
impl<'a> From<LifecycleContext<'a>> for HostEl {
#[track_caller]
fn from(ctx: LifecycleContext<'a>) -> Self {
check_phase(ctx.phase, ELEMENT_PHASES, "HostEl");
HostEl(ctx.el.parent_element().unwrap_or_else(|| ctx.el.clone()))
}
}
#[derive(Clone, Copy)]
pub struct IsTeleported(pub bool);
impl<'a> From<LifecycleContext<'a>> for IsTeleported {
#[track_caller]
fn from(ctx: LifecycleContext<'a>) -> Self {
check_phase(ctx.phase, ELEMENT_PHASES, "IsTeleported");
IsTeleported(crate::directives::teleport::host_of(ctx.el).is_some())
}
}
#[derive(Clone)]
pub struct ScopePath(pub Vec<ScopeId>);
impl<'a> From<LifecycleContext<'a>> for ScopePath {
fn from(ctx: LifecycleContext<'a>) -> Self {
let mut out = vec![ctx.scope_id];
let mut cur = ctx.scope_id;
while let Some(p) = crate::context::parent_of(cur) {
out.push(p);
cur = p;
}
ScopePath(out)
}
}
#[derive(Clone)]
pub struct TeleportHost(pub Option<Element>);
impl<'a> From<LifecycleContext<'a>> for TeleportHost {
#[track_caller]
fn from(ctx: LifecycleContext<'a>) -> Self {
check_phase(ctx.phase, ELEMENT_PHASES, "TeleportHost");
TeleportHost(crate::directives::teleport::host_of(ctx.el))
}
}
thread_local! {
static MOUNT_EPOCH_COUNTER: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
static MOUNT_EPOCHS: RefCell<HashMap<ScopeId, u64>> = RefCell::new(HashMap::new());
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct MountEpoch(pub u64);
impl<'a> From<LifecycleContext<'a>> for MountEpoch {
fn from(ctx: LifecycleContext<'a>) -> Self {
MountEpoch(MOUNT_EPOCHS.with(|m| {
let mut map = m.borrow_mut();
*map.entry(ctx.scope_id).or_insert_with(|| {
MOUNT_EPOCH_COUNTER.with(|c| {
let v = c.get();
c.set(v + 1);
v
})
})
}))
}
}
#[doc(hidden)]
pub fn __clear_mount_epoch(scope: ScopeId) {
MOUNT_EPOCHS.with(|m| {
m.borrow_mut().remove(&scope);
});
}
#[doc(hidden)]
pub fn __clear_mount_epochs(scopes: &[ScopeId]) {
if scopes.is_empty() {
return;
}
MOUNT_EPOCHS.with(|m| {
let mut map = m.borrow_mut();
if map.is_empty() {
return;
}
for scope in scopes {
map.remove(scope);
}
});
}
#[derive(Clone, Copy)]
pub struct Elapsed(pub f64);
impl<'a> From<LifecycleContext<'a>> for Elapsed {
fn from(_: LifecycleContext<'a>) -> Self {
Elapsed(js_sys::Date::now())
}
}
impl<'a, T: 'static> From<LifecycleContext<'a>> for crate::plugin::Plugin<T> {
fn from(_: LifecycleContext<'a>) -> Self {
crate::plugin::required_plugin::<T>()
}
}
impl<'a, T: 'static> From<LifecycleContext<'a>> for Option<crate::plugin::Plugin<T>> {
fn from(_: LifecycleContext<'a>) -> Self {
crate::plugin::active_plugin::<T>()
}
}