1use 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 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 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}