pocopine-core 0.1.0

Client-side reactive runtime for pocopine — a Rust/WASM port of Alpine.js.
Documentation
//! `HandlerDispatch` — the indirection the `#[component]` and `#[handlers]`
//! macros use to coordinate. `#[component]` emits a `ComponentState::invoke`
//! that delegates here; `#[handlers]` emits the actual match-by-name body.
//!
//! [`FromHandlerArg`] bridges `JsValue` → typed handler parameters.
//! Per RFC-008; the `#[handlers]` macro emits a conversion call per
//! arg using this trait, so method authors can write
//! `(&mut self, ev: InputEvent)` or `(&mut self, value: String)`
//! directly.

use js_sys::Array;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{
    ClipboardEvent, CustomEvent, DragEvent, Event, FocusEvent, InputEvent, KeyboardEvent,
    MouseEvent, PointerEvent, SubmitEvent, TouchEvent, UiEvent, WheelEvent,
};

pub trait HandlerDispatch {
    fn invoke_handler(&mut self, key: &str, args: &Array) -> JsValue;

    /// Called once after the scope is minted and the parent-context
    /// chain (RFC-027) is set up, **before** the template's children
    /// walk. The place to initialise fields that depend on injected
    /// state — e.g. a compound-component child reading its root's
    /// scope id via `inject` to compute a per-instance anchor
    /// selector. Analogous to Vue 3's `setup()`.
    ///
    /// Runs with `CURRENT_SCOPE_ID` bound so `inject` / `this`
    /// resolve. Override generated by `#[handlers]` for components
    /// that define `on_setup`. Phase tag is `Setup` —
    /// element-dependent extractors panic.
    fn setup(&mut self, ctx: crate::lifecycle::LifecycleContext<'_>) {
        let _ = ctx;
    }

    /// Called once after the component is mounted and its subtree is
    /// fully bound. The `#[handlers]` macro generates an override
    /// that delegates to the user's `on_mount` method when one
    /// exists. RFC-032: receives a `LifecycleContext` whose
    /// extractors the generated forwarder projects into the user
    /// method's declared parameters.
    fn mount(&mut self, ctx: crate::lifecycle::LifecycleContext<'_>) {
        let _ = ctx;
    }

    /// Called once, scheduled via `tick::next` AFTER `mount()` returns.
    /// Takes `&self` so proxy-reading helpers (`watch_field`, refs,
    /// `$event`) called from inside don't clash with an active
    /// `borrow_mut`. Mutation in on_ready goes via
    /// `pocopine::this::<Self>().update(...)`. See RFC-026 / RFC-029.
    /// RFC-032: receives a `LifecycleContext` — same story as
    /// `mount`.
    fn on_ready(&self, ctx: crate::lifecycle::LifecycleContext<'_>) {
        let _ = ctx;
    }

    /// Called once just before the component unmounts, while the
    /// state is still mutable and the scope is still in the registry.
    /// Override generated by `#[handlers]` when the user writes
    /// `on_unmount`. Phase tag is `Unmount` — element-dependent
    /// extractors panic (refs may already be cleared).
    fn unmount(&mut self, ctx: crate::lifecycle::LifecycleContext<'_>) {
        let _ = ctx;
    }

    /// True iff the component author actually wrote an `on_setup`
    /// method. `mount_component` uses this to decide whether to
    /// bind `CURRENT_SCOPE_ID` + invoke the hook pre-template-walk.
    fn has_setup(&self) -> bool {
        false
    }

    /// True iff the component author actually wrote an `on_mount`
    /// method. The mount uses this to decide whether to fire a
    /// `trigger_scope` sweep after [`mount`] — components with no
    /// hook should not pay for a sweep.
    fn has_on_mount(&self) -> bool {
        false
    }

    /// True iff the component author actually wrote an `on_ready`
    /// method. The mount uses this to decide whether to schedule
    /// the post-mount tick — components without the hook pay nothing.
    fn has_on_ready(&self) -> bool {
        false
    }

    /// Symmetric with [`has_on_mount`] for `on_unmount`. Currently
    /// informational — the mount doesn't sweep on unmount — but kept
    /// for parity and so future devtools can show per-component
    /// lifecycle coverage.
    fn has_on_unmount(&self) -> bool {
        false
    }

    /// RFC-038 — preset name the component animates with by default
    /// on enter (symmetric if `transition_out_preset` returns the
    /// same). Empty string means "no transition preset declared on
    /// this component". The `#[component(transition = "…")]` macro
    /// arg overrides; `transition_in = "…"` wins over `transition`
    /// for this side.
    fn transition_in_preset(&self) -> &'static str {
        ""
    }

    /// RFC-038 — preset name for the leave (out) phase. See
    /// [`Self::transition_in_preset`] for the override rules.
    fn transition_out_preset(&self) -> &'static str {
        ""
    }

    /// RFC-038 — keyed-pp-for layout animation kind. Currently
    /// `"flip"` is the only recognised value; everything else is a
    /// no-op (forwards-compatible). Empty string = no animate kind
    /// declared.
    fn animate_kind(&self) -> &'static str {
        ""
    }

    /// Component-level synthetic readonly fields generated from
    /// `#[computed]` methods. Empty by default.
    fn computed_keys() -> &'static [&'static str]
    where
        Self: Sized,
    {
        &[]
    }

    /// Resolve a synthetic computed field by name.
    fn computed_get(&self, key: &str) -> Option<JsValue> {
        let _ = key;
        None
    }
}

/// Convert a raw `JsValue` into the handler-argument type the author
/// declared. Returning `None` causes the `#[handlers]` macro to drop
/// the invocation — same silent-fail shape as "unknown key" in
/// `invoke_handler`.
///
/// Built-in impls cover:
/// * `JsValue` (identity),
/// * `Option<T>` — `None` when the slot is undefined / null,
/// * `String`, `bool`, `f64`, `f32`, `i32`, `i64`, `u32`, `u64`,
///   `usize`, `isize`,
/// * the common `web_sys::*Event` types (`Event`, `MouseEvent`,
///   `KeyboardEvent`, `InputEvent`, `FocusEvent`, `CustomEvent`,
///   `UiEvent`).
///
/// User types can participate by implementing the trait directly —
/// two lines for a `Deserialize` struct:
///
/// ```ignore
/// impl FromHandlerArg for MyThing {
///     fn from_handler_arg(v: JsValue) -> Option<Self> {
///         serde_wasm_bindgen::from_value(v).ok()
///     }
/// }
/// ```
pub trait FromHandlerArg: Sized {
    fn from_handler_arg(v: JsValue) -> Option<Self>;
}

impl FromHandlerArg for JsValue {
    fn from_handler_arg(v: JsValue) -> Option<Self> {
        Some(v)
    }
}

impl<T: FromHandlerArg> FromHandlerArg for Option<T> {
    fn from_handler_arg(v: JsValue) -> Option<Self> {
        if v.is_undefined() || v.is_null() {
            return Some(None);
        }
        Some(T::from_handler_arg(v))
    }
}

impl FromHandlerArg for String {
    fn from_handler_arg(v: JsValue) -> Option<Self> {
        v.as_string()
    }
}

impl FromHandlerArg for bool {
    fn from_handler_arg(v: JsValue) -> Option<Self> {
        v.as_bool()
    }
}

impl FromHandlerArg for f64 {
    fn from_handler_arg(v: JsValue) -> Option<Self> {
        v.as_f64()
    }
}

impl FromHandlerArg for f32 {
    fn from_handler_arg(v: JsValue) -> Option<Self> {
        v.as_f64().map(|n| n as f32)
    }
}

macro_rules! impl_int_handler_arg {
    ($($t:ty),*) => {$(
        impl FromHandlerArg for $t {
            fn from_handler_arg(v: JsValue) -> Option<Self> {
                v.as_f64().map(|n| n as $t)
            }
        }
    )*};
}
impl_int_handler_arg!(i32, i64, u32, u64, isize, usize);

macro_rules! impl_event_handler_arg {
    ($($t:ty),*) => {$(
        impl FromHandlerArg for $t {
            fn from_handler_arg(v: JsValue) -> Option<Self> {
                v.dyn_into::<$t>().ok()
            }
        }
    )*};
}
impl_event_handler_arg!(
    Event,
    UiEvent,
    MouseEvent,
    KeyboardEvent,
    InputEvent,
    FocusEvent,
    CustomEvent,
    // Pointer / drag / wheel / touch — direct manipulation flows.
    PointerEvent,
    DragEvent,
    WheelEvent,
    TouchEvent,
    // Form + clipboard — common UI plumbing.
    SubmitEvent,
    ClipboardEvent
);