use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use js_sys::{Object, Reflect};
use wasm_bindgen::closure::Closure;
use wasm_bindgen::JsCast;
use wasm_bindgen::JsValue;
use web_sys::{console, Element, Event, EventTarget};
use crate::expr::{self, Expr, Spanned};
use crate::loop_scope::LoopScope;
use crate::reactive::ScopeId;
use crate::scope::{with_current_el, Scope};
#[derive(Clone, Copy, Debug)]
pub enum BindingKind {
Text,
Class,
Html,
Bind { arg: &'static str },
Show,
}
#[doc(hidden)]
pub struct StaticBinding {
pub node_path: &'static [u16],
pub kind: BindingKind,
pub expr_src: &'static str,
pub compiled: Option<&'static expr::StaticExpr>,
}
#[doc(hidden)]
pub struct StaticListener {
pub node_path: &'static [u16],
pub event: &'static str,
pub modifiers: &'static [&'static str],
pub expr_src: &'static str,
}
#[doc(hidden)]
pub struct StaticRef {
pub node_path: &'static [u16],
pub name: &'static str,
}
#[doc(hidden)]
pub struct StaticNativeModel {
pub node_path: &'static [u16],
pub expr_src: &'static str,
pub number: bool,
pub lazy: bool,
}
#[doc(hidden)]
pub struct StaticChildMount {
pub node_path: &'static [u16],
pub tag: &'static str,
pub slots: &'static [StaticSlotFragment],
pub bindings: &'static [StaticChildHostBinding],
pub listeners: &'static [StaticChildHostListener],
pub models: &'static [StaticChildHostModel],
}
#[doc(hidden)]
pub struct StaticChildHostBinding {
pub arg: &'static str,
pub expr_src: &'static str,
pub compiled: Option<&'static expr::StaticExpr>,
}
#[doc(hidden)]
pub struct StaticChildHostListener {
pub event: &'static str,
pub modifiers: &'static [&'static str],
pub expr_src: &'static str,
}
#[doc(hidden)]
pub struct StaticChildHostModel {
pub arg: Option<&'static str>,
pub modifiers: &'static [&'static str],
pub expr_src: &'static str,
}
#[doc(hidden)]
pub struct StaticSlotFragment {
pub name: &'static str,
pub fragment: crate::slot_fragment::SlotFragment,
pub scoped_let: Option<&'static str>,
}
#[doc(hidden)]
pub struct StaticInterp {
pub node_path: &'static [u16],
pub text_index: u16,
pub segments: &'static [crate::directives::interp::PlannedSegment],
}
#[doc(hidden)]
pub struct StaticOpaqueDirective {
pub node_path: &'static [u16],
pub name: &'static str,
pub arg: Option<&'static str>,
pub modifiers: &'static [&'static str],
pub value: &'static str,
}
#[doc(hidden)]
pub struct StaticSlotOutlet {
pub node_path: &'static [u16],
pub name: &'static str,
}
#[doc(hidden)]
pub struct StaticIfPlan {
pub template_node_path: &'static [u16],
pub expr_src: &'static str,
pub compiled: Option<&'static expr::StaticExpr>,
pub teleport_selector: Option<&'static str>,
pub body: Option<IfBodyFn>,
}
pub type IfBodyFn =
fn(scope_id: ScopeId, proxy: &JsValue, ctx_parent_id: ScopeId) -> Option<web_sys::Element>;
#[doc(hidden)]
pub struct StaticTeleportPlan {
pub template_node_path: &'static [u16],
pub selector: &'static str,
pub body: Option<TeleportBodyFn>,
}
pub type TeleportBodyFn =
fn(scope_id: ScopeId, proxy: &JsValue, ctx_parent_id: ScopeId) -> Option<web_sys::Element>;
#[doc(hidden)]
pub struct StaticForPlan {
pub template_node_path: &'static [u16],
pub item_name: &'static str,
pub items_expr: &'static str,
pub key_expr: Option<&'static str>,
pub stagger_ms: u32,
pub body: Option<ForBodyFn>,
}
pub type ForBodyFn =
fn(scope_id: ScopeId, proxy: &JsValue, ctx_parent_id: ScopeId) -> Option<web_sys::Element>;
#[doc(hidden)]
pub struct StaticRowPlan {
pub plan_id: u32,
pub item_name: &'static str,
pub bindings: &'static [StaticBinding],
pub listeners: &'static [StaticListener],
}
pub(crate) struct CompiledBinding {
pub node_path: &'static [u16],
pub kind: BindingKind,
pub ast: Spanned<Expr>,
fast: Option<FastExpr>,
parent_field_paths: Box<[String]>,
}
pub(crate) struct CompiledListener {
pub node_path: &'static [u16],
pub event: &'static str,
pub ast: Spanned<Expr>,
}
pub struct CompiledRowPlan {
pub plan_id: u32,
pub item_name: &'static str,
pub(crate) bindings: Vec<CompiledBinding>,
pub(crate) listeners: Vec<CompiledListener>,
}
impl CompiledRowPlan {
pub(crate) fn is_proxy_elision_eligible(&self) -> bool {
self.bindings.iter().all(|b| b.fast.is_some())
}
}
thread_local! {
static ROW_PLANS: RefCell<HashMap<(String, u32), Rc<CompiledRowPlan>>> =
RefCell::new(HashMap::new());
}
pub fn register_row_plans(component_name: &str, plans: &'static [StaticRowPlan]) {
if plans.is_empty() {
return;
}
ROW_PLANS.with(|registry| {
let mut r = registry.borrow_mut();
for sp in plans {
let mut bindings: Vec<CompiledBinding> = Vec::with_capacity(sp.bindings.len());
let mut ok = true;
for b in sp.bindings {
match expr::parse_cached(b.expr_src) {
Ok(ast) => {
let fast = compile_fast_expr(sp.item_name, &ast);
let parent_field_paths =
collect_parent_field_paths(sp.item_name, &ast);
bindings.push(CompiledBinding {
node_path: b.node_path,
kind: b.kind,
ast,
fast,
parent_field_paths,
});
}
Err(e) => {
console::error_1(&JsValue::from_str(&format!(
"rfc-054: row plan {component_name}#{} binding parse failed (macro bug): {} at {}..{}",
sp.plan_id, e.message, e.span.start, e.span.end,
)));
ok = false;
break;
}
}
}
if !ok {
continue;
}
let mut listeners: Vec<CompiledListener> = Vec::with_capacity(sp.listeners.len());
for l in sp.listeners {
match expr::parse_cached(l.expr_src) {
Ok(ast) => listeners.push(CompiledListener {
node_path: l.node_path,
event: l.event,
ast,
}),
Err(e) => {
console::error_1(&JsValue::from_str(&format!(
"rfc-054: row plan {component_name}#{} listener parse failed (macro bug): {} at {}..{}",
sp.plan_id, e.message, e.span.start, e.span.end,
)));
ok = false;
break;
}
}
}
if !ok {
continue;
}
r.insert(
(component_name.to_string(), sp.plan_id),
Rc::new(CompiledRowPlan {
plan_id: sp.plan_id,
item_name: sp.item_name,
bindings,
listeners,
}),
);
}
});
}
#[derive(Clone)]
enum FastExpr {
Path(FastPath),
TernaryEq {
lhs: FastPath,
rhs: FastPath,
then_value: JsValue,
else_value: JsValue,
invert: bool,
},
}
#[derive(Clone)]
struct FastPath {
root: FastPathRoot,
keys: Box<[JsValue]>,
}
#[derive(Clone, Copy)]
enum FastPathRoot {
LoopItem,
LoopIndex,
LoopFirst,
LoopLast,
Parent,
}
fn collect_parent_field_paths(item_name: &str, expr: &Spanned<Expr>) -> Box<[String]> {
let mut out: Vec<String> = Vec::new();
walk_collect(item_name, expr, &mut out);
out.into_boxed_slice()
}
fn walk_collect(item_name: &str, expr: &Spanned<Expr>, out: &mut Vec<String>) {
match &expr.value {
Expr::Path(segments) => {
let Some(first) = segments.first() else {
return;
};
if first == item_name || first == "$index" || first == "$first" || first == "$last" {
return;
}
if !out.iter().any(|existing| existing == first) {
out.push(first.clone());
}
}
Expr::Not(inner) => walk_collect(item_name, inner, out),
Expr::BinOp(_, lhs, rhs) => {
walk_collect(item_name, lhs, out);
walk_collect(item_name, rhs, out);
}
Expr::Ternary(c, t, e) => {
walk_collect(item_name, c, out);
walk_collect(item_name, t, out);
walk_collect(item_name, e, out);
}
Expr::Call(_, args) => {
for arg in args {
walk_collect(item_name, arg, out);
}
}
Expr::Assign(_, rhs) => {
walk_collect(item_name, rhs, out);
}
Expr::Seq(stmts) => {
for s in stmts {
walk_collect(item_name, s, out);
}
}
Expr::Literal(_) => {}
}
}
fn compile_fast_expr(item_name: &str, expr: &Spanned<Expr>) -> Option<FastExpr> {
match &expr.value {
Expr::Path(segments) => Some(FastExpr::Path(compile_fast_path(item_name, segments)?)),
Expr::Ternary(cond, then_e, else_e) => {
let Expr::BinOp(op, lhs, rhs) = &cond.value else {
return None;
};
let invert = match op {
crate::expr::BinOp::Eq => false,
crate::expr::BinOp::Ne => true,
_ => return None,
};
let lhs = match &lhs.value {
Expr::Path(segments) => compile_fast_path(item_name, segments)?,
_ => return None,
};
let rhs = match &rhs.value {
Expr::Path(segments) => compile_fast_path(item_name, segments)?,
_ => return None,
};
Some(FastExpr::TernaryEq {
lhs,
rhs,
then_value: literal_fast_value(then_e)?,
else_value: literal_fast_value(else_e)?,
invert,
})
}
_ => None,
}
}
fn compile_fast_path(item_name: &str, segments: &[String]) -> Option<FastPath> {
let (root, rest) = match segments.first().map(String::as_str)? {
first if first == item_name => (FastPathRoot::LoopItem, &segments[1..]),
"$index" => (FastPathRoot::LoopIndex, &segments[1..]),
"$first" => (FastPathRoot::LoopFirst, &segments[1..]),
"$last" => (FastPathRoot::LoopLast, &segments[1..]),
_ => (FastPathRoot::Parent, segments),
};
Some(FastPath {
root,
keys: rest
.iter()
.map(|segment| JsValue::from_str(segment))
.collect::<Vec<_>>()
.into_boxed_slice(),
})
}
fn literal_fast_value(expr: &Spanned<Expr>) -> Option<JsValue> {
match &expr.value {
Expr::Literal(crate::expr::Literal::Null) => Some(JsValue::NULL),
Expr::Literal(crate::expr::Literal::Bool(v)) => Some(JsValue::from_bool(*v)),
Expr::Literal(crate::expr::Literal::Number(v)) => Some(JsValue::from_f64(*v)),
Expr::Literal(crate::expr::Literal::String(v)) => Some(JsValue::from_str(v)),
_ => None,
}
}
fn evaluate_binding(
binding: &CompiledBinding,
proxy: &JsValue,
loop_state: Option<&LoopScope>,
) -> JsValue {
if let (Some(fast), Some(loop_state)) = (&binding.fast, loop_state) {
return evaluate_fast_expr(fast, loop_state);
}
expr::evaluate(&binding.ast, proxy)
}
fn evaluate_fast_expr(expr: &FastExpr, loop_state: &LoopScope) -> JsValue {
match expr {
FastExpr::Path(path) => evaluate_fast_path(path, loop_state),
FastExpr::TernaryEq {
lhs,
rhs,
then_value,
else_value,
invert,
} => {
let eq = Object::is(
&evaluate_fast_path(lhs, loop_state),
&evaluate_fast_path(rhs, loop_state),
);
if eq ^ *invert {
then_value.clone()
} else {
else_value.clone()
}
}
}
}
fn fast_expr_depends_on_loop_position(expr: &FastExpr) -> bool {
match expr {
FastExpr::Path(path) => fast_path_depends_on_loop_position(path),
FastExpr::TernaryEq { lhs, rhs, .. } => {
fast_path_depends_on_loop_position(lhs) || fast_path_depends_on_loop_position(rhs)
}
}
}
fn fast_path_depends_on_loop_position(path: &FastPath) -> bool {
matches!(
path.root,
FastPathRoot::LoopIndex | FastPathRoot::LoopFirst | FastPathRoot::LoopLast
)
}
impl CompiledRowPlan {
pub(crate) fn depends_on_loop_position(&self) -> bool {
self.bindings.iter().any(|binding| {
binding
.fast
.as_ref()
.map(fast_expr_depends_on_loop_position)
.unwrap_or(true)
})
}
}
fn evaluate_fast_path(path: &FastPath, loop_state: &LoopScope) -> JsValue {
let mut cur = match path.root {
FastPathRoot::LoopItem => loop_state.item.clone(),
FastPathRoot::LoopIndex => JsValue::from_f64(loop_state.index as f64),
FastPathRoot::LoopFirst => JsValue::from_bool(loop_state.index == 0),
FastPathRoot::LoopLast => JsValue::from_bool(loop_state.index + 1 == loop_state.total),
FastPathRoot::Parent => loop_state.parent.clone(),
};
for key in path.keys.iter() {
cur = Reflect::get(&cur, key).unwrap_or(JsValue::UNDEFINED);
}
cur
}
pub(crate) fn ensure_delegated_listeners(
plan: &Rc<CompiledRowPlan>,
template_el: &Element,
parent_node: &web_sys::Node,
) {
let Some(parent_el) = parent_node.dyn_ref::<Element>() else {
return;
};
for event in unique_listener_events(plan) {
let marker = format!("data-pp-delegated-{event}");
if template_el.has_attribute(&marker) {
continue;
}
let _ = template_el.set_attribute(&marker, "");
install_list_delegated_listener(parent_el.clone(), event);
}
}
fn unique_listener_events(plan: &CompiledRowPlan) -> Vec<&'static str> {
let mut events: Vec<&'static str> = Vec::new();
for listener in &plan.listeners {
if !events.contains(&listener.event) {
events.push(listener.event);
}
}
events
}
fn install_list_delegated_listener(parent_el: Element, event: &'static str) {
let parent_for_track = parent_el.clone();
let closure = Closure::wrap(Box::new(move |ev: Event| {
let Some(target) = ev
.target()
.and_then(|target| target.dyn_into::<web_sys::Node>().ok())
else {
return;
};
let mut cursor = target.dyn_ref::<Element>().cloned();
while let Some(el) = cursor {
if let Some(scope_id) = crate::mount::scope_id_of_element(&el) {
dispatch_delegated_event(scope_id, event, &target, &ev);
return;
}
cursor = el.parent_element();
}
}) as Box<dyn FnMut(Event)>);
let target: EventTarget = parent_el.into();
crate::mount::track_listener_on(&parent_for_track, target, event, false, closure);
}
fn dispatch_delegated_event(
scope_id: ScopeId,
event: &'static str,
target: &web_sys::Node,
ev: &Event,
) {
ROW_INSTANCES.with(|m| {
let map = m.borrow();
let Some(instance) = map.get(&scope_id) else {
return;
};
let Some(proxy) = instance_proxy(instance, scope_id) else {
return;
};
let ev_js: JsValue = {
let r: &JsValue = ev.as_ref();
r.clone()
};
for route in instance.listener_routes.iter().rev() {
if route.event != event {
continue;
}
let route_node: &web_sys::Node = route.node.as_ref();
if !route_node.contains(Some(target)) {
continue;
}
with_current_el(&route.node, || {
crate::scope::with_current_scope_id(scope_id, || {
crate::magics::with_current_event(&ev_js, || {
expr::evaluate(&route.ast, &proxy);
});
});
});
}
});
}
pub fn lookup_for_template(template_el: &Element) -> Option<Rc<CompiledRowPlan>> {
let plan_id = template_el
.get_attribute("data-pp-row-plan")
.and_then(|s| s.parse::<u32>().ok())?;
let component_name = nearest_component_name(template_el)?;
ROW_PLANS.with(|r| r.borrow().get(&(component_name, plan_id)).cloned())
}
fn instance_proxy(instance: &RowInstance, scope_id: ScopeId) -> Option<JsValue> {
if let Some(p) = instance.proxy.borrow().as_ref() {
return Some(p.clone());
}
let scope = Scope::find(scope_id)?;
let proxy = scope.into_proxy();
*instance.proxy.borrow_mut() = Some(proxy.clone());
Some(proxy)
}
fn nearest_component_name(el: &Element) -> Option<String> {
let mut cur: Option<Element> = Some(el.clone());
while let Some(e) = cur {
if let Some(name) = e.get_attribute("data-pp-scope-id") {
return Some(name);
}
let tag = e.tag_name().to_ascii_lowercase();
if tag.contains('-') {
return Some(tag);
}
cur = e.parent_element();
}
None
}
struct RowInstance {
plan: Rc<CompiledRowPlan>,
binding_nodes: Box<[Element]>,
binding_cache: Vec<Option<Rc<str>>>,
proxy: RefCell<Option<JsValue>>,
loop_state: Option<Rc<RefCell<LoopScope>>>,
listener_routes: Box<[RowListenerRoute]>,
list_key: ListWatcherKey,
}
struct RowListenerRoute {
event: &'static str,
node: Element,
ast: Spanned<Expr>,
}
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
struct ListWatcherKey {
parent_scope_id: ScopeId,
plan_ptr: usize,
}
impl ListWatcherKey {
fn new(parent_scope_id: ScopeId, plan: &Rc<CompiledRowPlan>) -> Self {
Self {
parent_scope_id,
plan_ptr: Rc::as_ptr(plan) as usize,
}
}
}
struct ListWatcher {
effect_id: crate::reactive::EffectId,
members: Vec<ScopeId>,
}
thread_local! {
static ROW_INSTANCES: RefCell<HashMap<ScopeId, RowInstance>> =
RefCell::new(HashMap::new());
static LIST_WATCHERS: RefCell<HashMap<ListWatcherKey, ListWatcher>> =
RefCell::new(HashMap::new());
}
fn resolve_node_path(root: &Element, path: &[u16]) -> Option<Element> {
let mut cur: Element = root.clone();
for &idx in path {
let children = cur.children();
let next = children.item(idx as u32)?;
cur = next;
}
Some(cur)
}
pub(crate) fn mount_row_compiled(
plan: &Rc<CompiledRowPlan>,
row_root: &Element,
scope_id: ScopeId,
proxy: Option<&JsValue>,
) {
mount_rows_compiled(plan, &[(row_root.clone(), scope_id, proxy.cloned())]);
}
pub(crate) fn mount_rows_compiled(
plan: &Rc<CompiledRowPlan>,
rows: &[(Element, ScopeId, Option<JsValue>)],
) {
if rows.is_empty() {
return;
}
let mut instances: Vec<(ScopeId, RowInstance)> = Vec::with_capacity(rows.len());
let mut watched_members: Vec<ScopeId> = Vec::new();
let mut watcher: Option<(ListWatcherKey, JsValue)> = None;
let node_path_start = crate::profiler::mount::start();
let mut resolved_binding_nodes: Vec<Box<[Element]>> = Vec::with_capacity(rows.len());
for (row_root, _, _) in rows {
let mut binding_nodes: Vec<Element> = Vec::with_capacity(plan.bindings.len());
for b in &plan.bindings {
let Some(node) = resolve_node_path(row_root, b.node_path) else {
console::warn_1(&JsValue::from_str(&format!(
"rfc-054: row binding node_path {:?} did not resolve",
b.node_path,
)));
return;
};
binding_nodes.push(node);
}
resolved_binding_nodes.push(binding_nodes.into_boxed_slice());
}
crate::profiler::mount::record_node_path_resolution(node_path_start);
let binding_start = crate::profiler::mount::start();
let mut binding_caches: Vec<Vec<Option<Rc<str>>>> = Vec::with_capacity(rows.len());
let mut loop_states: Vec<Option<Rc<RefCell<LoopScope>>>> = Vec::with_capacity(rows.len());
let mut parent_links: Vec<Option<(ScopeId, JsValue)>> = Vec::with_capacity(rows.len());
for ((_, scope_id, proxy), binding_nodes) in rows.iter().zip(resolved_binding_nodes.iter()) {
let loop_state = Scope::find(*scope_id).and_then(|scope| scope.typed::<LoopScope>());
let parent_link = loop_state.as_ref().map(|state| {
let borrow = state.borrow();
(borrow.parent_scope_id, borrow.parent.clone())
});
let mut binding_cache: Vec<Option<Rc<str>>> = vec![None; plan.bindings.len()];
{
let loop_borrow = loop_state.as_ref().map(|state| state.borrow());
let loop_ref = loop_borrow.as_deref();
let placeholder_proxy = JsValue::UNDEFINED;
let proxy_for_eval = proxy.as_ref().unwrap_or(&placeholder_proxy);
for (i, b) in plan.bindings.iter().enumerate() {
if !b.parent_field_paths.is_empty() {
continue;
}
let v = evaluate_binding(b, proxy_for_eval, loop_ref);
binding_cache[i] = apply_binding(&binding_nodes[i], b.kind, &v, None);
}
}
binding_caches.push(binding_cache);
loop_states.push(loop_state);
parent_links.push(parent_link);
}
crate::profiler::mount::record_initial_binding_apply(binding_start);
let listener_start = crate::profiler::mount::start();
let mut listener_routes: Vec<Box<[RowListenerRoute]>> = Vec::with_capacity(rows.len());
for (row_root, _, _) in rows {
listener_routes.push(resolve_listener_routes(plan, row_root));
crate::profiler::mount::record_compiled_row_mounted();
}
crate::profiler::mount::record_listener_installation(listener_start);
for (
(((((_, scope_id, proxy), binding_nodes), binding_cache), listener_routes), loop_state),
parent_link,
) in rows
.iter()
.zip(resolved_binding_nodes.into_iter())
.zip(binding_caches.into_iter())
.zip(listener_routes.into_iter())
.zip(loop_states.into_iter())
.zip(parent_links.into_iter())
{
let initial_proxy: RefCell<Option<JsValue>> = RefCell::new(proxy.clone());
let list_key = match parent_link {
Some((parent_scope_id, parent_proxy)) => {
let list_key = ListWatcherKey::new(parent_scope_id, plan);
if watcher.is_none() {
watcher = Some((list_key, parent_proxy));
}
watched_members.push(*scope_id);
list_key
}
None => ListWatcherKey::new(ScopeId(0), plan),
};
instances.push((
*scope_id,
RowInstance {
plan: plan.clone(),
binding_nodes,
binding_cache,
proxy: initial_proxy,
loop_state,
listener_routes,
list_key,
},
));
}
ROW_INSTANCES.with(|m| {
let mut map = m.borrow_mut();
for (scope_id, instance) in instances {
map.insert(scope_id, instance);
}
});
if let Some((list_key, parent_proxy)) = watcher {
ensure_list_watcher(plan, list_key, parent_proxy);
LIST_WATCHERS.with(|m| {
if let Some(watcher) = m.borrow_mut().get_mut(&list_key) {
watcher.members.extend(watched_members.iter().copied());
}
});
refresh_parent_bindings_many(&watched_members);
}
}
fn ensure_list_watcher(
plan: &Rc<CompiledRowPlan>,
list_key: ListWatcherKey,
parent_proxy: JsValue,
) {
let already = LIST_WATCHERS.with(|m| m.borrow().contains_key(&list_key));
if !already {
let mut field_keys: Vec<JsValue> = Vec::new();
for binding in &plan.bindings {
for name in binding.parent_field_paths.iter() {
let key_js = JsValue::from_str(name);
let already_have = field_keys
.iter()
.any(|existing| existing.as_string().as_deref() == Some(name.as_str()));
if !already_have {
field_keys.push(key_js);
}
}
}
if field_keys.is_empty() {
return;
}
let parent_proxy_for_effect = parent_proxy.clone();
let list_key_for_effect = list_key;
let effect_id = crate::reactive::effect(move || {
for k in &field_keys {
let _ = Reflect::get(&parent_proxy_for_effect, k);
}
let members: Vec<ScopeId> = LIST_WATCHERS.with(|m| {
m.borrow()
.get(&list_key_for_effect)
.map(|w| w.members.clone())
.unwrap_or_default()
});
for row_scope in members {
refresh_parent_bindings(row_scope);
}
});
LIST_WATCHERS.with(|m| {
m.borrow_mut().insert(
list_key,
ListWatcher {
effect_id,
members: Vec::new(),
},
);
});
}
}
fn refresh_parent_bindings(scope_id: ScopeId) {
refresh_parent_bindings_many(&[scope_id]);
}
fn refresh_parent_bindings_many(scope_ids: &[ScopeId]) {
ROW_INSTANCES.with(|m| {
let mut map = m.borrow_mut();
for scope_id in scope_ids {
let Some(instance) = map.get_mut(scope_id) else {
continue;
};
let plan = instance.plan.clone();
let proxy_value = instance.proxy.borrow().clone();
let placeholder = JsValue::UNDEFINED;
let proxy_ref = proxy_value.as_ref().unwrap_or(&placeholder);
let loop_borrow = instance.loop_state.as_ref().map(|state| state.borrow());
let loop_ref = loop_borrow.as_deref();
for (i, binding) in plan.bindings.iter().enumerate() {
if binding.parent_field_paths.is_empty() {
continue;
}
let v = evaluate_binding(binding, proxy_ref, loop_ref);
let prev = instance.binding_cache[i].as_deref();
instance.binding_cache[i] =
apply_binding(&instance.binding_nodes[i], binding.kind, &v, prev);
}
}
});
}
pub(crate) fn reuse_row_compiled(scope_id: ScopeId) -> bool {
ROW_INSTANCES.with(|m| {
let mut map = m.borrow_mut();
let Some(instance) = map.get_mut(&scope_id) else {
return false;
};
let plan = instance.plan.clone();
let proxy_value = instance.proxy.borrow().clone();
let placeholder = JsValue::UNDEFINED;
let proxy_ref = proxy_value.as_ref().unwrap_or(&placeholder);
let loop_borrow = instance.loop_state.as_ref().map(|state| state.borrow());
let loop_ref = loop_borrow.as_deref();
for (i, b) in plan.bindings.iter().enumerate() {
let v = evaluate_binding(b, proxy_ref, loop_ref);
let prev = instance.binding_cache[i].as_deref();
instance.binding_cache[i] = apply_binding(&instance.binding_nodes[i], b.kind, &v, prev);
}
true
})
}
pub fn unmount_row_compiled(scope_id: ScopeId) {
let list_key = ROW_INSTANCES.with(|m| {
m.borrow_mut()
.remove(&scope_id)
.map(|instance| instance.list_key)
});
let Some(list_key) = list_key else {
return;
};
let dropped_effect = LIST_WATCHERS.with(|m| {
let mut map = m.borrow_mut();
let watcher = map.get_mut(&list_key)?;
watcher.members.retain(|id| *id != scope_id);
if watcher.members.is_empty() {
map.remove(&list_key).map(|w| w.effect_id)
} else {
None
}
});
if let Some(effect_id) = dropped_effect {
crate::reactive::release(effect_id);
}
}
pub fn unmount_rows_bulk(scope_ids: &[ScopeId]) {
if scope_ids.is_empty() {
return;
}
let mut list_keys: Vec<ListWatcherKey> = ROW_INSTANCES.with(|m| {
let mut map = m.borrow_mut();
scope_ids
.iter()
.filter_map(|id| map.remove(id).map(|instance| instance.list_key))
.collect()
});
if list_keys.is_empty() {
return;
}
list_keys.sort_unstable_by_key(|k| (k.parent_scope_id.0, k.plan_ptr));
list_keys.dedup();
let dropped_effects: Vec<crate::reactive::EffectId> = LIST_WATCHERS.with(|m| {
let mut map = m.borrow_mut();
list_keys
.iter()
.filter_map(|key| map.remove(key).map(|w| w.effect_id))
.collect()
});
for effect_id in dropped_effects {
crate::reactive::release(effect_id);
}
}
fn apply_binding(
el: &Element,
kind: BindingKind,
v: &JsValue,
prev: Option<&str>,
) -> Option<Rc<str>> {
match kind {
BindingKind::Text => {
let next = js_to_string(v);
if prev == Some(next.as_str()) {
return Some(Rc::from(next));
}
el.set_text_content(Some(&next));
Some(Rc::from(next))
}
BindingKind::Class => {
let serialised = serialise_class_value(v)?;
if prev == Some(serialised.as_str()) {
return Some(Rc::from(serialised));
}
if serialised.is_empty() {
let _ = el.remove_attribute("class");
} else {
let _ = el.set_attribute("class", &serialised);
}
Some(Rc::from(serialised))
}
BindingKind::Html | BindingKind::Bind { .. } | BindingKind::Show => {
console::warn_1(&JsValue::from_str(
"rfc-054: row plan received an RFC-058 template-plan-only \
BindingKind (Html / Bind / Show); skipping",
));
None
}
}
}
fn resolve_listener_routes(plan: &CompiledRowPlan, row_root: &Element) -> Box<[RowListenerRoute]> {
let mut routes: Vec<RowListenerRoute> = Vec::with_capacity(plan.listeners.len());
for listener in &plan.listeners {
let Some(node) = resolve_node_path(row_root, listener.node_path) else {
console::warn_1(&JsValue::from_str(&format!(
"rfc-054: row listener node_path {:?} did not resolve",
listener.node_path,
)));
continue;
};
routes.push(RowListenerRoute {
event: listener.event,
node,
ast: listener.ast.clone(),
});
}
routes.into_boxed_slice()
}
fn js_to_string(v: &JsValue) -> String {
if v.is_undefined() || v.is_null() {
return String::new();
}
v.as_string()
.or_else(|| v.as_f64().map(|n| n.to_string()))
.or_else(|| v.as_bool().map(|b| b.to_string()))
.unwrap_or_else(|| {
js_sys::JSON::stringify(v)
.ok()
.and_then(|s| s.as_string())
.unwrap_or_default()
})
}
fn serialise_class_value(v: &JsValue) -> Option<String> {
if v.is_undefined() || v.is_null() || v == &JsValue::FALSE {
return Some(String::new());
}
if let Some(s) = v.as_string() {
return Some(s);
}
if v.is_object() {
use js_sys::{Object, Reflect};
use wasm_bindgen::JsCast;
let obj: Object = v.clone().unchecked_into();
let keys = Object::keys(&obj);
let mut out: Vec<String> = Vec::new();
for i in 0..keys.length() {
let k = keys.get(i);
let truthy = Reflect::get(&obj, &k)
.map(|val| val.as_bool().unwrap_or(!val.is_falsy()))
.unwrap_or(false);
if truthy {
if let Some(s) = k.as_string() {
out.push(s);
}
}
}
return Some(out.join(" "));
}
None
}