1#![deny(missing_docs, missing_debug_implementations)]
117
118use dodrio::{builder, bumpalo, Node, Render, RenderContext};
119use js_sys::{Object, Promise, Reflect};
120use wasm_bindgen::prelude::*;
121use wasm_bindgen::JsCast;
122
123#[wasm_bindgen]
124extern "C" {
125 #[derive(Debug, Clone)]
133 pub type JsRender;
134 #[wasm_bindgen(structural, method)]
135 fn render(this: &JsRender) -> JsValue;
136
137 #[wasm_bindgen(extends = Object)]
138 #[derive(Debug, Clone)]
139 type Element;
140 #[wasm_bindgen(structural, getter, method, js_name = tagName)]
141 fn tag_name(this: &Element) -> String;
142 #[wasm_bindgen(structural, getter, method)]
143 fn listeners(this: &Element) -> js_sys::Array;
144 #[wasm_bindgen(structural, getter, method)]
145 fn attributes(this: &Element) -> js_sys::Array;
146 #[wasm_bindgen(structural, getter, method)]
147 fn children(this: &Element) -> js_sys::Array;
148
149 #[wasm_bindgen(extends = Object)]
150 #[derive(Debug, Clone)]
151 type Listener;
152 #[wasm_bindgen(structural, getter, method)]
153 fn on(this: &Listener) -> String;
154 #[wasm_bindgen(structural, getter, method)]
155 fn callback(this: &Listener) -> js_sys::Function;
156
157 #[wasm_bindgen(extends = Object)]
158 #[derive(Debug, Clone)]
159 type Attribute;
160 #[wasm_bindgen(structural, getter, method)]
161 fn name(this: &Attribute) -> String;
162 #[wasm_bindgen(structural, getter, method)]
163 fn value(this: &Attribute) -> String;
164}
165
166#[wasm_bindgen]
171#[derive(Clone, Debug)]
172pub struct VdomWeak {
173 inner: dodrio::VdomWeak,
174}
175
176impl VdomWeak {
177 fn new(inner: dodrio::VdomWeak) -> VdomWeak {
178 VdomWeak { inner }
179 }
180}
181
182#[wasm_bindgen]
183impl VdomWeak {
184 pub fn render(&self) -> Promise {
187 let future = self.inner.render();
188
189 wasm_bindgen_futures::future_to_promise(async move {
190 if let Err(e) = future.await {
191 let msg = e.to_string();
192 Err(js_sys::Error::new(&msg).into())
193 } else {
194 Ok(JsValue::null())
195 }
196 })
197 }
198}
199
200impl JsRender {
201 pub fn new<O>(object: O) -> JsRender
207 where
208 O: Into<Object>,
209 {
210 let object = object.into();
211 debug_assert!(
212 has_property(&object, "render"),
213 "JS rendering components must have a `render` method"
214 );
215 object.unchecked_into::<JsRender>()
216 }
217}
218
219impl<'a> Render<'a> for JsRender {
220 fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
221 create(cx, self.render())
222 }
223}
224
225fn has_property(obj: &Object, property: &str) -> bool {
226 Reflect::has(obj, &property.into()).unwrap_or_default()
227}
228
229fn create<'a>(cx: &mut RenderContext<'a>, val: JsValue) -> Node<'a> {
230 if let Some(txt) = val.as_string() {
231 let text = bumpalo::collections::String::from_str_in(&txt, cx.bump);
232 return builder::text(text.into_bump_str());
233 }
234
235 let elem = val.unchecked_into::<Element>();
236 debug_assert!(
237 elem.is_instance_of::<Object>(),
238 "JS render methods should only return strings for text nodes or objects for elements"
239 );
240 debug_assert!(
241 has_property(&elem, "tagName"),
242 "element objects returned by JS render methods must have a `tagName` property"
243 );
244
245 let tag_name = elem.tag_name();
246 let tag_name = bumpalo::collections::String::from_str_in(&tag_name, cx.bump);
247
248 builder::ElementBuilder::new(cx.bump, tag_name.into_bump_str())
249 .listeners({
250 let mut listeners =
251 bumpalo::collections::Vec::new_in(cx.bump);
252 if has_property(&elem, "listeners") {
253 let js_listeners = elem.listeners();
254 listeners.reserve(js_listeners.length() as usize);
255 js_listeners.for_each(&mut |listener, _index, _array| {
256 let listener = listener.unchecked_into::<Listener>();
257 debug_assert!(
258 listener.is_instance_of::<Object>(),
259 "listeners returned by JS render methods must be objects"
260 );
261 debug_assert!(
262 has_property(&listener, "on"),
263 "listener objects returned by JS render methods must have an `on` property"
264 );
265 debug_assert!(
266 has_property(&listener, "callback"),
267 "listener objects returned by JS render methods must have an `callback` property"
268 );
269 let on = listener.on();
270 let on = bumpalo::collections::String::from_str_in(&on, cx.bump);
271 let callback = listener.callback();
272 let elem = elem.clone();
273 listeners.push(builder::on(cx.bump, on.into_bump_str(), move |_root, vdom, event| {
274 let vdom = VdomWeak::new(vdom);
275 let vdom: JsValue = vdom.into();
276 if let Err(e) = callback.call2(&elem, &vdom, &event) {
277 wasm_bindgen::throw_val(e);
278 }
279 }));
280 });
281 }
282 listeners
283 })
284 .attributes({
285 let mut attributes = bumpalo::collections::Vec::new_in(cx.bump);
286 if has_property(&elem, "attributes") {
287 let js_attributes = elem.attributes();
288 attributes.reserve(js_attributes.length() as usize);
289 js_attributes.for_each(&mut |attribute, _index, _array| {
290 let attribute = attribute.unchecked_into::<Attribute>();
291 debug_assert!(
292 attribute.is_instance_of::<Object>(),
293 "attributes returned by JS render methods must be objects"
294 );
295 debug_assert!(
296 has_property(&attribute, "name"),
297 "attribute objects returned by JS render methods must have a `name` property"
298 );
299 debug_assert!(
300 has_property(&attribute, "value"),
301 "attribute objects returned by JS render methods must have a `value` property"
302 );
303 let name = attribute.name();
304 let name = bumpalo::collections::String::from_str_in(&name, cx.bump);
305 let value = attribute.value();
306 let value = bumpalo::collections::String::from_str_in(&value, cx.bump);
307 attributes.push(builder::attr(name.into_bump_str(), value.into_bump_str()));
308 });
309 }
310 attributes
311 })
312 .children({
313 let mut children = bumpalo::collections::Vec::new_in(cx.bump);
314 if has_property(&elem, "children") {
315 let js_children = elem.children();
316 children.reserve(js_children.length() as usize);
317 js_children.for_each(&mut |child, _index, _array| {
318 children.push(create(cx, child));
319 });
320 }
321 children
322 })
323 .finish()
324}