maia_wasm/ui/
macros.rs

1/// UI macro: define UI elements.
2///
3/// This macro is used as a convenience to define a `struct` called `Elements`
4/// that contains all the UI HTML elements. A constructor
5/// `Elements::new(document: &Document) -> Result<Elements, JsValue>` is also
6/// defined by this macro.
7///
8/// Each member in `Elements` is defined by its HTML id (which also gives the
9/// name of the member), the [`web_sys`] type of the corresponding HTML element,
10/// and the type to which it is transformed as a member of `Elements`. The
11/// latter is either an [`InputElement`](crate::ui::input::InputElement) or an
12/// [`Rc`](std::rc::Rc) wrapping the HTML element type. See the example below
13/// for details about the syntax.
14///
15/// # Example
16///
17/// ```
18/// use maia_wasm::{ui::input::{CheckboxInput, MHzPresentation, NumberInput},
19///                 ui_elements};
20/// use std::rc::Rc;
21/// use web_sys::{Document, HtmlButtonElement, HtmlInputElement};
22///
23/// ui_elements! {
24///     my_checkbox: HtmlInputElement => CheckboxInput,
25///     my_button: HtmlButtonElement => Rc<HtmlButtonElement>,
26///     my_frequency: HtmlInputElement => NumberInput<f32, MHzPresentation>,
27/// }
28///
29/// fn main() -> Result<(), wasm_bindgen::JsValue> {
30///     # // do not run the rest of the code during testing, as it will fail,
31///     # // but still check that it compiles
32///     # return Ok(());
33///     let (_, document) = maia_wasm::get_window_and_document()?;
34///     let elements = Elements::new(&document)?;
35///
36///     // elements.my_checkbox is a CheckboxInput
37///     // elements.my_button is an Rc<HtmlButtonElement>
38///     // etc.
39///
40///     Ok(())
41/// }
42/// ```
43#[macro_export]
44macro_rules! ui_elements {
45    {$($element:ident : $base_ty:ty => $transform_ty:ty),* $(,)?} => {
46        #[derive(Clone)]
47        struct Elements {
48            $(
49                $element: $transform_ty,
50            )*
51        }
52
53        impl Elements {
54            fn new(document: &web_sys::Document) -> Result<Elements, wasm_bindgen::JsValue> {
55                use wasm_bindgen::JsCast;
56                Ok(Elements {
57                    $(
58                        $element: std::rc::Rc::new(document
59                                          .get_element_by_id(stringify!($element))
60                                          .ok_or(concat!("failed to find ",
61                                                         stringify!($element),
62                                                         " element"))?
63                                          .dyn_into::<$base_ty>()?)
64                            .into(),
65                    )*
66                })
67            }
68        }
69    }
70}
71
72/// UI macro: implement an `onchange` method that calls an `apply` function.
73///
74/// This macro implements an `onchange` method for an element called
75/// `name`. The method is called `name_onchange`. It uses
76/// [`InputElement::get`](crate::ui::input::InputElement::get) to obtain the
77/// value of the element, updates the corresponding preferences item with this
78/// value, and calls a `name_apply` with this value. The `name_apply` method
79/// is defined by the user.
80///
81/// See [`onchange_apply_noprefs`](crate::onchange_apply_noprefs) for an example
82/// of usage.
83#[macro_export]
84macro_rules! onchange_apply {
85    ($($name:ident),*) => {
86        paste::paste! {
87            $(
88                fn [<$name _onchange>](&self) -> wasm_bindgen::closure::Closure<dyn Fn()> {
89                    use $crate::ui::input::InputElement;
90                    let ui = self.clone();
91                    wasm_bindgen::closure::Closure::new(move || {
92                        let element = &ui.elements.$name;
93                        if !element.report_validity() {
94                            return;
95                        }
96                        if let Some(value) = element.get() {
97                            ui.[<$name _apply>](value);
98                            // try_borrow_mut prevents trying to update the
99                            // preferences as a consequence of the
100                            // Preferences::apply_client calling this closure
101                            if let Ok(mut p) = ui.preferences.try_borrow_mut() {
102                                if let Err(e) = p.[<update_ $name>](&value) {
103                                    web_sys::console::error_1(&e);
104                                }
105                            }
106                        } else {
107                            ui.window
108                                .alert_with_message(concat!("Invalid value for ",
109                                                            stringify!($name)))
110                                .unwrap();
111                        }
112                    })
113                }
114            )*
115        }
116    }
117}
118
119/// UI macro: implement an `onchange` method that calls an `apply` method without
120/// updating the preferences.
121///
122/// This macro is similar to [`onchange_apply`], but it does not update the
123/// preferences.
124///
125/// # Example
126///
127/// ```
128/// use maia_wasm::{onchange_apply_noprefs, set_on, ui_elements,
129///                 ui::input::TextInput};
130/// use std::rc::Rc;
131/// use wasm_bindgen::JsValue;
132/// use web_sys::{Document, HtmlInputElement, Window};
133///
134/// #[derive(Clone)]
135/// struct Ui {
136///     window: Rc<Window>,
137///     elements: Elements,
138/// }
139///
140/// ui_elements! {
141///     my_text_field: HtmlInputElement => TextInput,
142/// }
143///
144/// impl Ui {
145///     fn new(window: Rc<Window>, document: &Document) -> Result<Ui, JsValue> {
146///         let elements = Elements::new(document)?;
147///         let ui = Ui { window, elements };
148///         ui.set_callbacks();
149///         Ok(ui)
150///     }
151///
152///     fn set_callbacks(&self) -> Result<(), JsValue> {
153///         set_on!(change, self, my_text_field);
154///         Ok(())
155///     }
156///
157///     onchange_apply_noprefs!(my_text_field);
158///
159///     fn my_text_field_apply(&self, value: String) {
160///         // do something with the value
161///         self.window.alert_with_message(&format!("got my_text_field = {value}"));
162///     }
163/// }
164/// ```
165#[macro_export]
166macro_rules! onchange_apply_noprefs {
167    ($($name:ident),*) => {
168        paste::paste! {
169            $(
170                fn [<$name _onchange>](&self) -> wasm_bindgen::closure::Closure<dyn Fn()> {
171                    use $crate::ui::input::InputElement;
172                    let ui = self.clone();
173                    wasm_bindgen::closure::Closure::new(move || {
174                        let element = &ui.elements.$name;
175                        if !element.report_validity() {
176                            return;
177                        }
178                        if let Some(value) = element.get() {
179                            ui.[<$name _apply>](value);
180                        } else {
181                            ui.window
182                                .alert_with_message(concat!("Invalid value for ",
183                                                            stringify!($name)))
184                                .unwrap();
185                        }
186                    })
187                }
188            )*
189        }
190    }
191}
192
193/// UI macro: set event callback for UI elements.
194///
195/// Given an `event` (for instance `change` or `click`) and assuming that there
196/// are methods called `element_onevent` for each element, this macro sets the
197/// event callbacks of each of the elements to the closures returned by these
198/// methods.
199///
200/// # Example
201///
202/// ```
203/// use maia_wasm::{onchange_apply_noprefs, set_on, ui_elements,
204///                 ui::input::{InputElement, TextInput}};
205/// use std::rc::Rc;
206/// use wasm_bindgen::{closure::Closure, JsCast, JsValue};
207/// use web_sys::{Document, HtmlButtonElement, HtmlInputElement, Window};
208///
209/// #[derive(Clone)]
210/// struct Ui {
211///     window: Rc<Window>,
212///     elements: Elements,
213/// }
214///
215/// ui_elements! {
216///     my_text_field_a: HtmlInputElement => TextInput,
217///     my_text_field_b: HtmlInputElement => TextInput,
218///     my_button_a: HtmlButtonElement => Rc<HtmlButtonElement>,
219///     my_button_b: HtmlButtonElement => Rc<HtmlButtonElement>,
220/// }
221///
222/// impl Ui {
223///     fn new(window: Rc<Window>, document: &Document) -> Result<Ui, JsValue> {
224///         let elements = Elements::new(document)?;
225///         let ui = Ui { window, elements };
226///         ui.set_callbacks();
227///         Ok(ui)
228///     }
229///
230///     fn set_callbacks(&self) -> Result<(), JsValue> {
231///         set_on!(change, self, my_text_field_a, my_text_field_b);
232///         set_on!(click, self, my_button_a, my_button_b);
233///         Ok(())
234///     }
235///
236///    fn my_text_field_a_onchange(&self) -> Closure<dyn Fn()> {
237///        let element = self.elements.my_text_field_a.clone();
238///        let window = self.window.clone();
239///        Closure::new(move || {
240///            if let Some(text) = element.get() {
241///                window.alert_with_message(&format!("my_text_field_a changed: value = {text}"));
242///            }
243///        })
244///    }
245///
246///    fn my_text_field_b_onchange(&self) -> Closure<dyn Fn()> {
247///        let element = self.elements.my_text_field_b.clone();
248///        let window = self.window.clone();
249///        Closure::new(move || {
250///            if let Some(text) = element.get() {
251///                window.alert_with_message(&format!("my_text_field_b changed: value = {text}"));
252///            }
253///        })
254///    }
255///
256///    fn my_button_a_onclick(&self) -> Closure<dyn Fn()> {
257///        let window = self.window.clone();
258///        Closure::new(move || {
259///            window.alert_with_message("my_button_a has been clicked");
260///        })
261///    }
262///
263///    fn my_button_b_onclick(&self) -> Closure<dyn Fn()> {
264///        let window = self.window.clone();
265///        Closure::new(move || {
266///            window.alert_with_message("my_button_b has been clicked");
267///        })
268///    }
269/// }
270/// ```
271#[macro_export]
272macro_rules! set_on {
273    ($event:ident, $self:expr, $($element:ident),*) => {
274        paste::paste! {
275            $(
276                $self.elements.$element.[<set_on $event>](Some(
277                    wasm_bindgen::JsCast::unchecked_ref(
278                        &$self.[<$element _on $event>]()
279                        .into_js_value())
280                ));
281            )*
282        }
283    }
284}
285
286// This is called by impl_patch and impl_put and not to be called directly.
287#[doc(hidden)]
288#[macro_export]
289macro_rules! impl_request {
290    ($name:ident, $request_json:ty, $get_json:ty, $url:expr, $method_ident:ident, $method:expr) => {
291        paste::paste! {
292            async fn [<$method_ident _ $name>](&self, json: &$request_json) -> Result<$get_json, $crate::ui::request::RequestError> {
293                use wasm_bindgen::JsCast;
294                let method = $method;
295                let request = $crate::ui::request::json_request($url, json, method)?;
296                let response = wasm_bindgen_futures::JsFuture::from(self.window.fetch_with_request(&request))
297                    .await?
298                    .dyn_into::<web_sys::Response>()?;
299                if !response.ok() {
300                    let status = response.status();
301                    let error: maia_json::Error = $crate::ui::request::response_to_json(&response).await?;
302                    match error.suggested_action {
303                        maia_json::ErrorAction::Ignore => {}
304                        maia_json::ErrorAction::Log =>
305                            web_sys::console::error_1(&format!(
306                                "{method} request failed with HTTP code {status}. \
307                                 Error description: {}", error.error_description).into()),
308                        maia_json::ErrorAction::Alert => {
309                            web_sys::console::error_1(&format!(
310                                "{method} request failed with HTTP code {status}. \
311                                 UI alert suggested. Error description: {}", error.error_description).into());
312                            self.alert(&error.error_description)?;
313                        }
314                    }
315                    return Err($crate::ui::request::RequestError::RequestFailed(error));
316                }
317                Ok($crate::ui::request::response_to_json(&response).await?)
318            }
319        }
320    };
321}
322
323/// UI macro: implements a method to send a PATCH request.
324///
325/// Given a `name`, this macro implements a `patch_name` method that sends a
326/// PATCH request to a `url`. The response of the PATCH is then parsed as JSON,
327/// and the resulting Rust value is returned.
328///
329/// The patch method signature is `async fn patch_name(&self, json:
330/// &$request_json) -> Result<$get_json, RequestError>`.
331///
332/// # Example
333///
334/// ```
335/// use maia_wasm::{impl_patch, ui::request::ignore_request_failed};
336/// use serde::{Deserialize, Serialize};
337/// use std::rc::Rc;
338/// use wasm_bindgen::JsValue;
339/// use web_sys::Window;
340///
341/// // An object defined by a REST API. This corresponds to the GET method.
342/// #[derive(Debug, Serialize, Deserialize)]
343/// struct MyObject {
344///     my_value: u64,
345/// }
346///
347/// // An object defined by a REST API. This corresponds to the PATCH method.
348/// #[derive(Debug, Serialize, Deserialize)]
349/// struct PatchMyObject {
350///     #[serde(skip_serializing_if = "Option::is_none")]
351///     my_value: Option<u64>,
352/// }
353///
354/// #[derive(Clone)]
355/// struct Ui {
356///     window: Rc<Window>,
357/// }
358///
359/// impl Ui {
360///     fn new(window: Rc<Window>) -> Ui {
361///         Ui { window }
362///     }
363///
364///     // it is necessary to define an alert method like this
365///     // to show errors to the user
366///     fn alert(&self, message: &str) -> Result<(), JsValue> {
367///         self.window.alert_with_message(message);
368///         Ok(())
369///     }
370///
371///     impl_patch!(my_object, PatchMyObject, MyObject, "/my_object");
372/// }
373///
374/// async fn example() -> Result<(), JsValue> {
375///     # // do not run the rest of the code during testing, as it will fail,
376///     # // but still check that it compiles
377///     # return Ok(());
378///     let (window, _) = maia_wasm::get_window_and_document()?;
379///     let ui = Ui::new(Rc::clone(&window));
380///
381///     let patch = PatchMyObject { my_value: Some(42) };
382///     // server errors are already handled by patch_my_object by calling
383///     // Ui::alert, so we can ignore them here.
384///     if let Some(result) = ignore_request_failed(ui.patch_my_object(&patch).await)? {
385///         window.alert_with_message(&format!("request result: {result:?}"));
386///     }
387///
388///     Ok(())
389/// }
390#[macro_export]
391macro_rules! impl_patch {
392    ($name:ident, $patch_json:ty, $get_json:ty, $url:expr) => {
393        $crate::impl_request!($name, $patch_json, $get_json, $url, patch, "PATCH");
394    };
395}
396
397/// UI macro: implements a method to send a PUT request.
398///
399/// Given a `name`, this macro implements a `put_name` method that sends a
400/// PUT request to a `url`. The response of the PUT is then parsed as JSON,
401/// and the resulting Rust value is returned.
402///
403/// The patch method signature is `async fn put_name(&self, json:
404/// &$request_json) -> Result<$get_json, RequestError>`.
405///
406/// This macro is very similar to [`impl_patch`]. See its documentation for an
407/// example.
408#[macro_export]
409macro_rules! impl_put {
410    ($name:ident, $put_json:ty, $get_json:ty, $url:expr) => {
411        $crate::impl_request!($name, $put_json, $get_json, $url, put, "PUT");
412    };
413}
414
415// This macro is not to be called directly by the user. It must only be called
416// through impl_update_elements and similar macros.
417#[doc(hidden)]
418#[macro_export]
419macro_rules! set_values_if_inactive {
420    ($self:expr, $source:expr, $section:ident, $($element:ident),*) => {
421        use $crate::ui::{active::IsElementActive, input::InputElement};
422        let mut preferences = $self.preferences.borrow_mut();
423        paste::paste!{
424            $(
425                // A checkbox HtmlInputElement is always considered inactive,
426                // because the user interaction with it is limited to clicking
427                // (rather than typing). Therefore, we update it regardless of
428                // whether it has focus.
429                if !$self.document.is_element_active(stringify!([<$section _ $element>]))
430                    || std::any::Any::type_id(&$self.elements.[<$section _ $element>])
431                    == std::any::TypeId::of::<$crate::ui::input::CheckboxInput>() {
432                    $self.elements.[<$section _ $element>].set(&$source.$element);
433                }
434                if let Err(e) = preferences.[<update_ $section _ $element>](&$source.$element) {
435                    web_sys::console::error_1(&e);
436                }
437            )*
438        }
439    }
440}
441
442// This macro is not to be called directly by the user. It must only be called
443// through impl_update_elements and similar macros.
444#[doc(hidden)]
445#[macro_export]
446macro_rules! set_values {
447    ($self:expr, $source:expr, $section:ident, $($element:ident),*) => {
448        use $crate::ui::input::InputElement;
449        let mut preferences = $self.preferences.borrow_mut();
450        paste::paste! {
451            $(
452                $self.elements.[<$section _ $element>].set(&$source.$element);
453                if let Err(e) = preferences.[<update_ $section _ $element>](&$source.$element) {
454                    web_sys::console::error_1(&e);
455                }
456            )*
457        }
458    }
459}
460
461/// UI macro: implements methods to update UI elements.
462///
463/// Given a `name`, this macro implements the methods
464/// `update_name_inactive_elements` and `update_name_all_elements`. Both methods
465/// have the signature `fn ...(&self, json: &$json) -> Result<(), JsValue>`.
466///
467/// These methods are intended to be called when a JSON response has been
468/// received, in order to keep the values of the UI elements synchronized with
469/// their server-side values.
470///
471/// The difference between the `_inactive_elements` and `_all_elements` methods
472/// is that the former does not update active elements, in order to avoid
473/// overriding the input that the user might be typing in a field.
474///
475/// The functions call a `post_update_name_elements` method with the same
476/// signature, which can implement any custom functionality. If no custom
477/// functionality is needed, a dummy method that does nothing can be implemented
478/// with the [`impl_post_update_noop`](crate::impl_post_update_noop) macro.
479///
480/// # Example
481///
482/// ```
483/// use maia_wasm::{impl_dummy_preferences, impl_post_update_noop, impl_update_elements,
484///                 ui_elements, ui::input::NumberInput};
485/// use serde::{Deserialize, Serialize};
486/// use std::{cell::RefCell, rc::Rc};
487/// use wasm_bindgen::JsValue;
488/// use web_sys::{Document, HtmlInputElement, Window};
489///
490/// // An object defined by a REST API. This corresponds to the GET method.
491/// #[derive(Debug, Serialize, Deserialize)]
492/// struct MyObject {
493///     my_integer: u64,
494///     my_float: f32,
495/// }
496///
497/// #[derive(Clone)]
498/// struct Ui {
499///     window: Rc<Window>,
500///     document: Rc<Document>,
501///     elements: Elements,
502///     preferences: Rc<RefCell<Preferences>>,
503/// }
504///
505/// ui_elements! {
506///     my_object_my_integer: HtmlInputElement => NumberInput<u64>,
507///     my_object_my_float: HtmlInputElement => NumberInput<f32>,
508/// }
509///
510/// // Dummy Preferences struct. This is needed because update_elements
511/// // keeps the preferences in sync, so it calls methods in the Preferences.
512/// struct Preferences {}
513/// impl_dummy_preferences!(
514///     my_object_my_integer: u64,
515///     my_object_my_float: f32,
516/// );
517///
518/// impl Ui {
519///     fn new(window: Rc<Window>, document: Rc<Document>) -> Result<Ui, JsValue> {
520///         let elements = Elements::new(&document)?;
521///         let preferences = Rc::new(RefCell::new(Preferences {}));
522///         let ui = Ui { window, document, elements, preferences };
523///         Ok(ui)
524///     }
525///
526///     impl_update_elements!(my_object, MyObject, my_integer, my_float);
527///     impl_post_update_noop!(my_object, MyObject);
528/// }
529///
530/// fn main() -> Result<(), JsValue> {
531///     # // do not run the rest of the code during testing, as it will fail,
532///     # // but still check that it compiles
533///     # return Ok(());
534///     let (window, document) = maia_wasm::get_window_and_document()?;
535///     let ui = Ui::new(window, document)?;
536///
537///     // assume that the following data has come from a REST API GET
538///     let response = MyObject { my_integer: 42, my_float: 3.14 };
539///     ui.update_my_object_all_elements(&response);
540///
541///     Ok(())
542/// }
543/// ```
544#[macro_export]
545macro_rules! impl_update_elements {
546    ($name:ident, $json:ty, $($element:ident),*) => {
547        paste::paste! {
548            fn [<update_ $name _inactive_elements>](&self, json: &$json) -> Result<(), JsValue> {
549                $crate::set_values_if_inactive!(
550                    self,
551                    json,
552                    $name,
553                    $(
554                        $element
555                    ),*
556                );
557                self.[<post_update_ $name _elements>](json)
558            }
559
560            #[allow(dead_code)]
561            fn [<update_ $name _all_elements>](&self, json: &$json) -> Result<(), JsValue> {
562                $crate::set_values!(
563                    self,
564                    json,
565                    $name,
566                    $(
567                        $element
568                    ),*
569                );
570                self.[<post_update_ $name _elements>](json)
571            }
572        }
573    }
574}
575
576/// UI macro: implements a dummy `post_update_name_elements` method.
577///
578/// See [`impl_update_elements`] for more details.
579#[macro_export]
580macro_rules! impl_post_update_noop {
581    ($name:ident, $json:ty) => {
582        paste::paste! {
583            fn [<post_update_ $name _elements>](&self, _json: &$json) -> Result<(), JsValue> {
584                Ok(())
585            }
586        }
587    };
588}
589
590/// UI macro: implements a dummy `post_patch_name_update_elements` method.
591///
592/// See [`impl_section_custom`](crate::impl_section_custom) for more details.
593#[macro_export]
594macro_rules! impl_post_patch_update_elements_noop {
595    ($name:ident, $patch_json:ty) => {
596        paste::paste! {
597            fn [<post_patch_ $name _update_elements>](&self, _json: &$patch_json) -> Result<(), JsValue> {
598                Ok(())
599            }
600        }
601    }
602}
603
604/// UI macro: implements a dummy `name_onchange_patch_modify` method.
605///
606/// See [`impl_section_custom`](crate::impl_section_custom) for more details.
607#[macro_export]
608macro_rules! impl_onchange_patch_modify_noop {
609    ($name:ident, $patch_json:ty) => {
610        paste::paste! {
611            fn [<$name _onchange_patch_modify>](&self, _json: &mut $patch_json) {}
612        }
613    };
614}
615
616/// UI macro: implements a REST API section with default functionality.
617///
618/// This UI macro calls [`impl_section_custom`](crate::impl_section_custom) and
619/// it additionally calls the following macros to define dummy user-provided
620/// functions that do nothing:
621///
622/// - [`impl_post_update_noop`](crate::impl_post_update_noop), which implements
623///   a dummy `post_update_name_elements` method.
624///
625/// - [`impl_post_patch_update_elements_noop`], which implements a dummy
626///   `post_patch_name_update_elements` method.
627///
628/// - [`impl_onchange_patch_modify_noop`], which implements a dummy
629///   `name_onchange_patch_modify` method.
630///
631/// If custom functionality needs to be used in any of these methods,
632/// `impl_section_custom` needs to be used instead of `impl_section`. The
633/// `impl_*_noop` macros can still be called to implement the methods that do
634/// not need custom functionality.
635///
636/// # Example
637///
638/// # Example
639///
640/// ```
641/// use maia_wasm::{impl_dummy_preferences, impl_section, set_on, ui_elements, ui::input::NumberInput};
642/// use serde::{Deserialize, Serialize};
643/// use std::{cell::RefCell, rc::Rc};
644/// use wasm_bindgen::JsValue;
645/// use web_sys::{Document, HtmlInputElement, Window};
646///
647/// // An object defined by a REST API. This corresponds to the GET method.
648/// #[derive(Debug, Clone, Serialize, Deserialize)]
649/// struct MyObject {
650///     my_integer: u64,
651///     my_float: f32,
652/// }
653///
654/// // An object defined by a REST API. This corresponds to the PATCH method.
655/// #[derive(Debug, Clone, Default, Serialize, Deserialize)]
656/// struct PatchMyObject {
657///     #[serde(skip_serializing_if = "Option::is_none")]
658///     my_integer: Option<u64>,
659///     #[serde(skip_serializing_if = "Option::is_none")]
660///     my_float: Option<f32>,
661/// }
662///
663/// #[derive(Clone)]
664/// struct Ui {
665///     window: Rc<Window>,
666///     document: Rc<Document>,
667///     elements: Elements,
668///     // the api_state is optional; it could be defined
669///     // as Rc<RefCell<Option<()>> and set to contain None
670///     api_state: Rc<RefCell<Option<ApiState>>>,
671///     preferences: Rc<RefCell<Preferences>>,
672/// }
673///
674/// ui_elements! {
675///     my_object_my_integer: HtmlInputElement => NumberInput<u64>,
676///     my_object_my_float: HtmlInputElement => NumberInput<f32>,
677/// }
678///
679/// struct ApiState {
680///     my_object: MyObject,
681/// }
682///
683/// // Dummy Preferences struct. This is needed because update_elements
684/// // keeps the preferences in sync, so it calls methods in the Preferences.
685/// struct Preferences {}
686/// impl_dummy_preferences!(
687///     my_object_my_integer: u64,
688///     my_object_my_float: f32,
689/// );
690///
691/// impl Ui {
692///     fn new(window: Rc<Window>, document: Rc<Document>) -> Result<Ui, JsValue> {
693///         let elements = Elements::new(&document)?;
694///         let preferences = Rc::new(RefCell::new(Preferences {}));
695///         let api_state = Rc::new(RefCell::new(Some(ApiState {
696///             my_object: MyObject { my_integer: 0, my_float: 0.0 },
697///         })));
698///         let ui = Ui { window, document, elements, api_state, preferences };
699///         ui.set_callbacks();
700///         Ok(ui)
701///     }
702///
703///     fn set_callbacks(&self) -> Result<(), JsValue> {
704///         set_on!(change,
705///                 self,
706///                 my_object_my_integer,
707///                 my_object_my_float);
708///         Ok(())
709///     }
710///
711///     // it is necessary to define an alert method like this
712///     // to show errors to the user
713///     fn alert(&self, message: &str) -> Result<(), JsValue> {
714///         self.window.alert_with_message(message);
715///         Ok(())
716///     }
717///
718///     impl_section!(my_object,
719///                   MyObject,
720///                   PatchMyObject,
721///                   "/my_object",
722///                   my_integer,
723///                   my_float);
724/// }
725///
726/// ```
727#[macro_export]
728macro_rules! impl_section {
729    ($name:ident, $json:ty, $patch_json:ty, $url:expr, $($element:ident),*) => {
730        $crate::impl_post_update_noop!($name, $json);
731        $crate::impl_post_patch_update_elements_noop!($name, $patch_json);
732        $crate::impl_onchange_patch_modify_noop!($name, $patch_json);
733        $crate::impl_section_custom!($name, $json, $patch_json, $url, $($element),*);
734    }
735}
736
737/// UI macro: implements a REST API section using custom functionality.
738///
739/// This UI macro is used to implement the interaction between the UI and a
740/// section of the REST API using some custom functions. See [`impl_section`]
741/// for the equivalent without custom functions.
742///
743/// This macro includes the following:
744///
745/// - [`impl_patch`] call to implement a method that sends a PATCH request.
746///
747/// - [`impl_update_elements`] call to implement methods to update the UI elements.
748///
749/// - [`impl_onchange`](crate::impl_onchange) call to implement `onchange`
750///   methods for each element that perform a PATCH request and update all the UI
751///   elements of this section using the result.
752///
753/// - Implements a `patch_name_update_elements` of signature `async fn
754///   patch_name_update_elements(&self, json: &$patch_json) -> Result<(),
755///   JsValue>` that updates the `api_state` member of the UI with the `json`
756///   data, calls the `update_name_all_elements` method to update the contents of
757///   UI elements to match the server-side state, and calls the user-defined
758///   `post_patch_name_update_elements` with the `json` to perform any required
759///   user-defined functionality.
760///
761/// See [`impl_section`] for an example.
762#[macro_export]
763macro_rules! impl_section_custom {
764    ($name:ident, $json:ty, $patch_json:ty, $url:expr, $($element:ident),*) => {
765        $crate::impl_patch!($name, $patch_json, $json, $url);
766
767        $crate::impl_update_elements!(
768            $name,
769            $json,
770            $(
771                $element
772            ),*
773        );
774
775        $crate::impl_onchange!(
776            $name,
777            $patch_json,
778            $(
779                $element
780            ),*
781        );
782
783        paste::paste! {
784            async fn [<patch_ $name _update_elements>](&self, json: &$patch_json) -> Result<(), wasm_bindgen::JsValue> {
785                if let Some(json_output) = $crate::ui::request::ignore_request_failed(self.[<patch_ $name>](json).await)? {
786                    if let Some(state) = self.api_state.borrow_mut().as_mut() {
787                        state.$name.clone_from(&json_output);
788                    }
789                    self.[<update_ $name _all_elements>](&json_output)?;
790                    self.[<post_patch_ $name _update_elements>](json)?;
791                }
792                Ok(())
793            }
794        }
795    }
796}
797
798/// UI macro: implements a `_onchange` methods that perform a PATCH request.
799///
800/// Given a `name` and several `element`s, this macro implements
801/// `name_element_onchange` methods with signature `fn
802/// name_element_onchange(&self) -> Closure<dyn Fn() -> JsValue>`, whose return
803/// value can be used as the `onchange` closure for each of the elements. The
804/// closure obtains the Rust value of the element using
805/// [`InputElement::get`](crate::ui::input::InputElement::get), creates a PATCH
806/// object of type `$patch_json` that contains `element` set to a `Some`
807/// containing the obtained value and the remaining members set to `None` (their
808/// [`Default::default`] value), passes the `$patch_json` object to a
809/// user-defined `name_onchange_patch_modify` method that can modify the value
810/// of this object, and finally calls and awaits `patch_name_update_elements`,
811/// which performs the PATCH request and updates the UI elements using the
812/// response.
813#[macro_export]
814macro_rules! impl_onchange {
815    ($name:ident, $patch_json:ty, $($element:ident),*) => {
816        paste::paste! {
817            $(
818                fn [<$name _ $element _onchange>](&self) -> wasm_bindgen::closure::Closure<dyn Fn() -> wasm_bindgen::JsValue> {
819                    use $crate::ui::input::InputElement;
820                    let ui = self.clone();
821                    wasm_bindgen::closure::Closure::new(move || {
822                        if !ui.elements.[<$name _ $element>].report_validity() {
823                            return wasm_bindgen::JsValue::NULL;
824                        }
825                        let Some(value) = ui.elements.[<$name _ $element>].get() else {
826                            // TODO: decide what to do in this case, since it passes validity
827                            ui.window
828                                .alert_with_message(concat!(
829                                    "Invalid value for ", stringify!([<$name _ $element>])))
830                                .unwrap();
831                            return wasm_bindgen::JsValue::NULL;
832                        };
833                        #[allow(clippy::needless_update)]
834                        let mut patch = $patch_json { $element: Some(value), ..Default::default() };
835                        ui.[<$name _onchange_patch_modify>](&mut patch);
836                        let ui = ui.clone();
837                        wasm_bindgen_futures::future_to_promise(async move {
838                            ui.[<patch_ $name _update_elements>](&patch).await?;
839                            Ok(wasm_bindgen::JsValue::NULL)
840                        }).into()
841                    })
842                }
843            )*
844        }
845    }
846}
847
848/// UI macro: implements methods for an interface with tabs.
849///
850/// This macro implements a method `hide_all_tab_panels` with signature `fn
851/// hide_all_tab_panels(&self) -> Result<(), JsValue>` that sets, for each
852/// `element` given in the macro call, the corresponding `element_panel` to
853/// hidden by adding `hidden` to its class list, and the corresponding
854/// `element_tab` to deselected by seting its `aria-selected` attribute to
855/// `false`. For each `element`, it implements an `element_tab_onclick` method
856/// of signature `fn element_tab_onclick(&self) -> Closure<dyn Fn()>`. The
857/// closure returned by this method is a suitable `onclick` callback for the tab
858/// element. It calls the `hide_all_tab_panels` method and then selects the tab
859/// corresponding to `element` by removing the `hidden` class from the
860/// `element_panel` and setting the `arial-selected` attribute to `true` in the
861/// `element_tab`.
862///
863/// # Example
864///
865/// ```
866/// use maia_wasm::{impl_tabs, set_on, ui_elements};
867/// use std::rc::Rc;
868/// use wasm_bindgen::JsValue;
869/// use web_sys::{Document, HtmlButtonElement, HtmlElement, Window};
870///
871/// #[derive(Clone)]
872/// struct Ui {
873///     window: Rc<Window>,
874///     elements: Elements,
875/// }
876///
877/// ui_elements! {
878///     a_tab: HtmlButtonElement => Rc<HtmlButtonElement>,
879///     a_panel: HtmlElement => Rc<HtmlElement>,
880///     b_tab: HtmlButtonElement => Rc<HtmlButtonElement>,
881///     b_panel: HtmlElement => Rc<HtmlElement>,
882/// }
883///
884/// impl Ui {
885///     fn new(window: Rc<Window>, document: &Document) -> Result<Ui, JsValue> {
886///         let elements = Elements::new(document)?;
887///         let ui = Ui { window, elements };
888///         ui.set_callbacks();
889///         Ok(ui)
890///     }
891///
892///     fn set_callbacks(&self) -> Result<(), JsValue> {
893///         set_on!(click, self, a_tab, b_tab);
894///         Ok(())
895///     }
896///
897///     impl_tabs!(a, b);
898/// }
899#[macro_export]
900macro_rules! impl_tabs {
901    ($($element:ident),*) => {
902        paste::paste! {
903            fn hide_all_tab_panels(&self) -> Result<(), wasm_bindgen::JsValue> {
904                $(
905                    self.elements.[<$element _panel>].class_list().add_1("hidden")?;
906                    self.elements.[<$element _tab>].set_attribute("aria-selected", "false")?;
907                )*
908                Ok(())
909            }
910
911            $(
912                fn [<$element _tab_onclick>](&self) -> wasm_bindgen::closure::Closure<dyn Fn()> {
913                    let ui = self.clone();
914                    wasm_bindgen::closure::Closure::new(move || {
915                        ui.hide_all_tab_panels().unwrap();
916                        ui.elements.[<$element _panel>].class_list().remove_1("hidden").unwrap();
917                        ui.elements.[<$element _tab>].set_attribute("aria-selected", "true").unwrap();
918                    })
919                }
920            )*
921        }
922    }
923}