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
//! `SlotScope` — the per-materialization scope bound to scoped-slot
//! content. Per RFC-011.
//!
//! Exposes a single user-named identifier (the `pp-let` value) whose
//! JS-object shape is `{ prop_1: resolve_path(owner, path_1), ... }`
//! — i.e. the `:prop="path"` bindings the component author declared
//! on the `<slot>` element. Everything else falls through to the
//! owner scope, so a scoped-slot template can still reach `$store`,
//! `$route`, and the component's own fields.
//!
//! Read-only: slot scopes don't write. Mutating the bound values
//! flows through the owner component's `$dispatch` / handler surface.
use js_sys::{Array, Object, Reflect};
use wasm_bindgen::JsValue;
use crate::path::resolve_path;
use crate::reactive::ScopeId;
use crate::scope::ComponentState;
pub struct SlotScope {
/// The `pp-let` identifier (e.g. `"ctx"`).
pub ident: String,
/// Each `:prop="path"` pair declared on the `<slot>`. The prop
/// name lands under the ident's object; the path resolves on
/// [`Self::bind_source`] at read time.
pub bindings: Vec<(String, String)>,
/// The scope whose template contains the `<slot>` element. Used
/// to resolve `:prop="path"` binding paths — `path` was
/// authored in that scope (e.g. `<slot :ctx="current">` reads
/// `current` from the component that declared the slot).
pub bind_source: JsValue,
/// The scope whose template *authored* the slot content. Used
/// for fall-through identifier reads — `@click="parent_handler"`
/// inside the slot has to resolve against the caller, not the
/// component that declared the slot.
pub caller: JsValue,
/// Scope id of the caller — `caller`'s proxy alone isn't enough
/// to dispatch handlers because [`crate::scope::invoke_handler`]
/// works off ids, not proxies. Without this, `@click="handler"`
/// inside a scoped slot would hit [`SlotScope::invoke`] (which
/// only knows the proxy) and the call would silently no-op.
pub caller_scope_id: ScopeId,
}
impl ComponentState for SlotScope {
fn get(&self, key: &str) -> JsValue {
if key == self.ident.as_str() {
let obj = Object::new();
for (prop, path) in &self.bindings {
let val = resolve_path(&self.bind_source, path);
let _ = Reflect::set(&obj, &JsValue::from_str(prop), &val);
}
return obj.into();
}
// Fall through to the caller for everything else — handlers,
// parent-scope fields, magics. This is what makes
// `@click="parent_handler"` work inside a slot's template.
Reflect::get(&self.caller, &JsValue::from_str(key)).unwrap_or(JsValue::UNDEFINED)
}
// Slot scopes derive their value from a parent proxy on every
// call, so caching would freeze the slot at its first-render
// snapshot. Reactivity rides through the parent's per-key
// trigger picked up by the inner `resolve_path` / `Reflect::get`
// calls above.
fn cacheable_fields(&self) -> bool {
false
}
fn set(&mut self, _key: &str, _value: JsValue) {
// Slot scopes are read-only.
}
fn keys(&self) -> &'static [&'static str] {
&[]
}
fn invoke(&mut self, key: &str, args: &Array) -> JsValue {
// Delegate to the caller's scope so handler expressions
// inside scoped slots — `@click="parent_handler"` — reach
// the component that authored the slot content. The slot
// scope itself owns no handlers; without this delegation,
// handler dispatch would silently no-op for every compiled
// and mount-driven scoped slot listener.
crate::scope::invoke_handler(self.caller_scope_id, key, args)
}
}