wasm_react/macros.rs
1/// A convenience macro to [`create_element()`](crate::create_element()) for
2/// creating HTML element nodes.
3///
4/// Returns an [`H<HtmlTag>`](crate::props::H) struct that provides
5/// auto-completion for HTML attributes and events.
6///
7/// # Example
8///
9/// ```
10/// # use wasm_react::*;
11/// # fn f() -> VNode {
12/// h!(div)
13/// .attr("id", &"app".into())
14/// .build(
15/// h!(h1).build("Hello World!")
16/// )
17/// # }
18///
19/// // <div id="app"><h1>Hello World!</h1></div>
20///
21/// # fn g() -> VNode {
22/// h!("web-component")
23/// .build("Hello World!")
24/// # }
25///
26/// // <web-component>Hello World!</web-component>
27/// ```
28///
29/// It is also possible to add an id and/or classes to the element using a terse
30/// notation. You can use the same syntax as [`classnames!`](crate::classnames!).
31///
32/// ```
33/// # use wasm_react::*;
34/// # fn f() -> VNode {
35/// h!(div[#"app"."some-class"."warning"])
36/// .build("This is a warning!")
37/// # }
38///
39/// // <div id="app" class="some-class warning">This is a warning!</div>
40/// ```
41#[macro_export]
42macro_rules! h {
43 ($tag:literal $( [$( #$id:literal )? $( .$( $classnames:tt )+ )?] )?) => {
44 $crate::props::H::new($crate::props::HtmlTag($tag)) $(
45 $( .id($id) )?
46 $( .class_name(&$crate::classnames![.$( $classnames )+]) )?
47 )?
48 };
49 ($tag:ident $( [$( #$id:literal )? $( .$( $classnames:tt )+ )?] )?) => {
50 $crate::props::H::new($crate::props::HtmlTag(stringify!($tag))) $(
51 $( .id($id) )?
52 $( .class_name(&$crate::classnames![.$( $classnames )+]) )?
53 )?
54 };
55}
56
57/// A helper macro which can be used to clone a list of variables. Helpful for
58/// creating a closure which clone-captures the environment.
59///
60/// # Example
61///
62/// ```
63/// # use wasm_react::{*, hooks::*};
64/// # struct C { message: &'static str }
65/// # impl C { fn f(&self) {
66/// let switch = use_state(|| true);
67/// let counter = use_state(|| 0);
68///
69/// let cb = Callback::new({
70/// clones!(self.message, switch, mut counter);
71///
72/// move |delta: i32| {
73/// if (*switch.value()) {
74/// println!("{}", message);
75/// }
76///
77/// counter.set(|c| c + delta);
78/// }
79/// });
80/// # }}
81/// ```
82///
83/// This is equivalent to the following:
84///
85/// ```
86/// # use wasm_react::{*, hooks::*};
87/// # struct C { message: &'static str }
88/// # impl C { fn f(&self) {
89/// let switch = use_state(|| true);
90/// let counter = use_state(|| 0);
91///
92/// let cb = Callback::new({
93/// let message = self.message.clone();
94/// let switch = switch.clone();
95/// let mut counter = counter.clone();
96///
97/// move |delta: i32| {
98/// if (*switch.value()) {
99/// println!("{}", message);
100/// }
101///
102/// counter.set(|c| c + delta);
103/// }
104/// });
105/// # }}
106/// ```
107#[macro_export]
108macro_rules! clones {
109 (@clones $(,)? mut $obj:ident.$id:ident $( $tail:tt )*) => {
110 let mut $id = $obj.$id.clone();
111 $crate::clones!(@clones $( $tail )*);
112 };
113 (@clones $(,)? $obj:ident.$id:ident $( $tail:tt )*) => {
114 let $id = $obj.$id.clone();
115 $crate::clones!(@clones $( $tail )*);
116 };
117 (@clones $(,)? mut $id:ident $( $tail:tt )*) => {
118 let mut $id = $id.clone();
119 $crate::clones!(@clones $( $tail )*);
120 };
121 (@clones $(,)? $id:ident $( $tail:tt )*) => {
122 let $id = $id.clone();
123 $crate::clones!(@clones $( $tail )*);
124 };
125 (@clones) => {};
126
127 ($( $tt:tt )*) => {
128 $crate::clones!(@clones $( $tt )*);
129 };
130}
131
132/// Constructs a [`String`] based on various types that implement
133/// [`Classnames`](crate::props::Classnames).
134///
135/// # Example
136///
137/// ```
138/// # use wasm_react::*;
139/// assert_eq!(
140/// classnames![."button"."blue"],
141/// "button blue ".to_string(),
142/// );
143///
144/// let blue = false;
145/// let disabled = true;
146///
147/// assert_eq!(
148/// classnames![."button".blue.disabled],
149/// "button disabled ".to_string(),
150/// );
151///
152/// let is_blue = Some("blue");
153/// let disabled = "disabled".to_string();
154///
155/// assert_eq!(
156/// classnames![."button".{is_blue}.{disabled}],
157/// "button blue disabled ",
158/// );
159/// ```
160#[macro_export]
161macro_rules! classnames {
162 [@single $result:ident <<] => {};
163
164 // Handle string literals
165 [@single $result:ident << .$str:literal $( $tail:tt )*] => {
166 $crate::props::Classnames::append_to(&$str, &mut $result);
167 $crate::classnames![@single $result << $( $tail ) *];
168 };
169
170 // Handle boolean variables
171 [@single $result:ident << .$bool:ident $( $tail:tt )*] => {
172 $crate::props::Classnames::append_to(
173 &$bool.then(|| stringify!($bool)),
174 &mut $result
175 );
176 $crate::classnames![@single $result << $( $tail ) *];
177 };
178
179 // Handle block expressions
180 [@single $result:ident << .$block:block $( $tail:tt )*] => {
181 $crate::props::Classnames::append_to(&$block, &mut $result);
182 $crate::classnames![@single $result << $( $tail ) *];
183 };
184
185 [] => {
186 ::std::string::String::new()
187 };
188 [$( $tt:tt )*] => {
189 {
190 let mut result = ::std::string::String::new();
191 $crate::classnames![@single result << $( $tt )*];
192 result
193 }
194 };
195}
196
197/// This macro can be used to expose your [`Component`](crate::Component) for JS
198/// consumption via `wasm-bindgen`.
199///
200/// Requirement is that you implement the [`TryFrom<JsValue, Error = JsValue>`](core::convert::TryFrom)
201/// trait on your component and that you do not export anything else that has
202/// the same name as your component.
203///
204/// Therefore, it is only recommended to use this macro if you're writing a
205/// library for JS consumption only, or if you're writing a standalone
206/// application, since this will pollute the export namespace, which isn't
207/// desirable if you're writing a library for Rust consumption only.
208///
209/// # Example
210///
211/// Implement [`TryFrom<JsValue, Error = JsValue>`](core::convert::TryFrom) on
212/// your component and export it:
213///
214/// ```
215/// # use wasm_react::*;
216/// # use wasm_bindgen::prelude::*;
217/// # use js_sys::Reflect;
218/// #
219/// pub struct Counter {
220/// counter: i32,
221/// }
222///
223/// impl Component for Counter {
224/// # fn render(&self) -> VNode { VNode::new() }
225/// /* … */
226/// }
227///
228/// impl TryFrom<JsValue> for Counter {
229/// type Error = JsValue;
230///
231/// fn try_from(value: JsValue) -> Result<Self, Self::Error> {
232/// let diff = Reflect::get(&value, &"counter".into())?
233/// .as_f64()
234/// .ok_or(JsError::new("`counter` property not found"))?;
235///
236/// Ok(Counter { counter: diff as i32 })
237/// }
238/// }
239///
240/// export_components! { Counter }
241/// ```
242///
243/// In JS, you can use it like any other component:
244///
245/// ```js
246/// import React from "react";
247/// import init, { Counter } from "./path/to/pkg/project.js";
248///
249/// function SomeOtherJsComponent(props) {
250/// return (
251/// <div>
252/// <Counter counter={0} />
253/// </div>
254/// );
255/// }
256/// ```
257///
258/// You can export multiple components and also rename them:
259///
260/// ```
261/// # use wasm_react::*;
262/// # use wasm_bindgen::prelude::*;
263/// # pub struct App; pub struct Counter;
264/// # impl Component for App { fn render(&self) -> VNode { VNode::new() } }
265/// # impl TryFrom<JsValue> for App {
266/// # type Error = JsValue;
267/// # fn try_from(_: JsValue) -> Result<Self, Self::Error> { todo!() }
268/// # }
269/// # impl Component for Counter { fn render(&self) -> VNode { VNode::new() } }
270/// # impl TryFrom<JsValue> for Counter {
271/// # type Error = JsValue;
272/// # fn try_from(_: JsValue) -> Result<Self, Self::Error> { todo!() }
273/// # }
274/// export_components! {
275/// /// Some doc comment for the exported component.
276/// App as CounterApp,
277/// Counter
278/// }
279/// ```
280#[macro_export]
281macro_rules! export_components {
282 {} => {};
283 {
284 $( #[$meta:meta] )*
285 $Component:ident $( , $( $tail:tt )* )?
286 } => {
287 $crate::export_components! {
288 $( #[$meta] )*
289 $Component as $Component $( , $( $tail )* )?
290 }
291 };
292 {
293 $( #[$meta:meta] )*
294 $Component:ty as $Name:ident $( , $( $tail:tt )* )?
295 } => {
296 $crate::paste! {
297 $( #[$meta] )*
298 #[allow(non_snake_case)]
299 #[allow(dead_code)]
300 #[doc(hidden)]
301 #[::wasm_bindgen::prelude::wasm_bindgen(js_name = $Name)]
302 pub fn [<__WasmReact_Export_ $Name>](
303 props: ::wasm_bindgen::JsValue,
304 ) -> ::wasm_bindgen::JsValue
305 where
306 $Component: $crate::Component
307 + TryFrom<::wasm_bindgen::JsValue, Error = ::wasm_bindgen::JsValue>
308 {
309 let component_ref = $crate::hooks::use_memo({
310 let props = props.clone();
311
312 move || $Component::try_from(props).unwrap()
313 }, $crate::hooks::Deps::some(props));
314
315 $crate::react_bindings::use_rust_tmp_refs();
316
317 let component = component_ref.value();
318 $crate::Component::render(&*component).into()
319 }
320 }
321
322 $( $crate::export_components! { $( $tail )* } )?
323 };
324}
325
326/// This macro can be used to import JS React components for Rust consumption
327/// via `wasm-bindgen`.
328///
329/// Make sure that the components you import use the same React runtime as
330/// specified for `wasm-react`.
331///
332/// # Example
333///
334/// Assume the JS components are defined and exported in `/.dummy/myComponents.js`:
335///
336/// ```js
337/// import "https://unpkg.com/react/umd/react.production.min.js";
338///
339/// export function MyComponent(props) { /* … */ }
340/// export function PublicComponent(props) { /* … */ }
341/// export function RenamedComponent(props) { /* … */ }
342/// ```
343///
344/// Then you can import them using `import_components!`:
345///
346/// ```
347/// # use wasm_react::*;
348/// # use wasm_bindgen::prelude::*;
349/// import_components! {
350/// #[wasm_bindgen(module = "/.dummy/myComponents.js")]
351///
352/// /// Some doc comment for the imported component.
353/// MyComponent,
354/// /// This imported component will be made public.
355/// pub PublicComponent,
356/// /// You can rename imported components.
357/// RenamedComponent as pub OtherComponent,
358/// }
359/// ```
360///
361/// Now you can include the imported components in your render function:
362///
363/// ```
364/// # use wasm_react::{*, props::*};
365/// # use wasm_bindgen::prelude::*;
366/// # import_components! { #[wasm_bindgen(inline_js = "")] MyComponent }
367/// # struct App;
368/// # impl Component for App {
369/// fn render(&self) -> VNode {
370/// h!(div).build(
371/// MyComponent::new()
372/// .attr("prop", &"Hello World!".into())
373/// .build(())
374/// )
375/// }
376/// # }
377/// ```
378///
379/// # Defining Custom Convenience Methods
380///
381/// `MyComponent::new()` returns an [`H<MyComponent>`](crate::props::H) which
382/// can be used to define convenience methods by using a new extension trait:
383///
384/// ```
385/// # use wasm_react::{*, props::*};
386/// # use wasm_bindgen::prelude::*;
387/// # import_components! { #[wasm_bindgen(inline_js = "")] MyComponent }
388/// trait HMyComponentExt {
389/// fn prop(self, value: &str) -> Self;
390/// }
391///
392/// impl HMyComponentExt for H<MyComponent> {
393/// fn prop(self, value: &str) -> Self {
394/// self.attr("prop", &value.into())
395/// }
396/// }
397///
398/// /* … */
399///
400/// # struct App;
401/// # impl Component for App {
402/// fn render(&self) -> VNode {
403/// h!(div).build(
404/// MyComponent::new()
405/// .prop("Hello World!")
406/// .build(())
407/// )
408/// }
409/// # }
410/// ```
411#[macro_export]
412macro_rules! import_components {
413 { #[$from:meta] } => {};
414 {
415 #[$from:meta]
416 $( #[$meta:meta] )*
417 $vis:vis $Component:ident $( , $( $tail:tt )* )?
418 } => {
419 $crate::import_components! {
420 #[$from]
421 $( #[$meta] )*
422 $Component as $vis $Component $( , $( $tail )* )?
423 }
424 };
425 {
426 #[$from:meta]
427 $( #[$meta:meta] )*
428 $Component:ident as $vis:vis $Name:ident $( , $( $tail:tt )* )?
429 } => {
430 $crate::paste! {
431 #[$from]
432 extern "C" {
433 #[wasm_bindgen::prelude::wasm_bindgen(js_name = $Component)]
434 static [<__WASMREACT_IMPORT_ $Name:upper>]: wasm_bindgen::JsValue;
435 }
436
437 $( #[$meta] )*
438 #[derive(Debug, Clone, Copy)]
439 $vis struct $Name;
440
441 impl $Name {
442 #[doc = "Returns an `H<" $Name ">` struct that provides convenience "
443 "methods for adding props."]
444 pub fn new() -> $crate::props::H<$Name> {
445 $crate::props::H::new($Name)
446 }
447 }
448
449 impl $crate::props::HType for $Name {
450 fn as_js(&self) -> std::borrow::Cow<'_, JsValue> {
451 std::borrow::Cow::Borrowed(&[<__WASMREACT_IMPORT_ $Name:upper>])
452 }
453 }
454 }
455
456 $( $crate::import_components! { #[$from] $( $tail )* } )?
457 };
458}