1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
//! `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
);