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}