Skip to main content

fusor/dom/
property.rs

1//! Explicit custom-element properties, activated with their owning scope.
2use super::*;
3use wasm_bindgen::prelude::wasm_bindgen;
4
5#[wasm_bindgen(inline_js = r#"
6const unset = Symbol('fusor unset property');
7const definitions = new WeakMap();
8function report(s, error) {
9  if (!s.active) return;
10  console.error(error);
11  s.element.dispatchEvent(new CustomEvent('fusor:error', {detail: error, bubbles: true}));
12}
13function flush(s) {
14  if (!s.active || !s.ready || s.value === unset || Object.is(s.written, s.value)) return;
15  const value = s.value;
16  s.written = value;
17  try {
18    if (!Reflect.set(s.element, s.name, value)) throw new TypeError(`Cannot set custom element property ${s.name}`);
19  } catch (error) {
20    if (s.active && Object.is(s.written, value)) s.written = unset;
21    throw error;
22  }
23}
24export function property_create(element, name) {
25  return {element, name, value: unset, written: unset, active: false, ready: false, pending: null};
26}
27export function property_value(s, value) { s.value = value; flush(s); }
28export function property_activate(s) {
29  s.active = true;
30  const registry = s.element.ownerDocument.defaultView.customElements;
31  const name = s.element.localName;
32  if (registry.get(name)) {
33    registry.upgrade(s.element);
34    s.ready = true;
35    flush(s);
36  } else {
37    let tags = definitions.get(registry);
38    if (!tags) { tags = new Map(); definitions.set(registry, tags); }
39    let pending = tags.get(name);
40    if (!pending) {
41      pending = new Set();
42      tags.set(name, pending);
43      // Keep an empty entry until definition: repeated mount/drop must not add
44      // unbounded callbacks to the registry's never-settled promise.
45      registry.whenDefined(name).then(() => {
46        tags.delete(name);
47        for (const binding of pending) {
48          pending.delete(binding);
49          binding.pending = null;
50          if (!binding.active) continue;
51          try {
52            registry.upgrade(binding.element);
53            binding.ready = true;
54            flush(binding);
55          } catch (error) { report(binding, error); }
56        }
57      }).catch(error => {
58        tags.delete(name);
59        for (const binding of pending) { binding.pending = null; report(binding, error); }
60        pending.clear();
61      });
62    }
63    pending.add(s);
64    s.pending = pending;
65  }
66}
67export function property_dispose(s) {
68  s.active = false;
69  if (s.pending) { s.pending.delete(s); s.pending = null; }
70  s.element = null;
71  s.value = s.written = unset;
72}
73"#)]
74extern "C" {
75    fn property_create(element: &Element, name: &str) -> JsValue;
76    #[wasm_bindgen(catch)]
77    fn property_value(binding: &JsValue, value: &JsValue) -> Result<(), JsValue>;
78    #[wasm_bindgen(catch)]
79    fn property_activate(binding: &JsValue) -> Result<(), JsValue>;
80    fn property_dispose(binding: &JsValue);
81}
82
83struct Property(JsValue);
84impl Drop for Property {
85    fn drop(&mut self) {
86        property_dispose(&self.0);
87    }
88}
89
90impl Scope {
91    /// Assign a JavaScript value to a custom element property after activation
92    /// and definition. Objects retain identity; equal values skip the setter.
93    /// Pending definitions retain neither the element nor Rust state after drop.
94    /// A setter can have arbitrary external effects and is not transactional.
95    pub fn property<V: Into<JsValue>>(
96        &mut self,
97        target: impl ElementTarget,
98        name: &str,
99        read: impl Fn() -> V + 'static,
100    ) -> Result<(), JsValue> {
101        if self.is_coherent() {
102            return Err(JsValue::from_str(
103                "fusor: custom properties cannot participate in coherent rendering",
104            ));
105        }
106        let element = target.resolve(self)?;
107        if !element.local_name().contains('-') || !is_html(&element) || name.is_empty() {
108            return Err(JsValue::from_str(
109                "fusor: prop:name requires a custom element and a nonempty property name",
110            ));
111        }
112        let property = Rc::new(Property(property_create(&element, name)));
113        let update = property.clone();
114        self.bind(move || property_value(&update.0, &read().into()))?;
115        let activate = property.clone();
116        let registration = self.owner().on_activate(move || {
117            if let Err(error) = property_activate(&activate.0) {
118                web_sys::console::error_1(&error);
119            }
120        });
121        // Cleanup must clear the JS handle even if another Rust owner retains
122        // registrations until the scope itself drops.
123        let cleanup = property.clone();
124        let registration_cleanup = self
125            .owner()
126            .on_cleanup(move || property_dispose(&cleanup.0));
127        self.retain((property, registration, registration_cleanup));
128        Ok(())
129    }
130}