vertigo/
lib.rs

1//! Vertigo is a library for building reactive web components.
2//!
3//! It mainly consists of four parts:
4//!
5//! * **Reactive dependencies** - A graph of values and clients (micro-subscriptions)
6//!   that can automatically compute what to refresh after one or more value change(s)
7//! * **Real DOM** - No intermediate Virtual DOM mechanism is necessary
8//! * **HTML/CSS macros** - Allows to construct Real DOM nodes using HTML and CSS
9//! * **Server-side rendering** - Out of the box when using `vertigo-cli`
10//!
11//! ## Example 1
12//!
13//! ```rust
14//! use vertigo::{dom, DomNode, Value, bind, main};
15//!
16//! #[main]
17//! pub fn app() -> DomNode {
18//!     let count = Value::new(0);
19//!
20//!     let increment = bind!(count, |_| {
21//!         count.change(|value| {
22//!             *value += 1;
23//!         });
24//!     });
25//!
26//!     let decrement = bind!(count, |_| {
27//!         count.change(|value| {
28//!             *value -= 1;
29//!         });
30//!     });
31//!
32//!     dom! {
33//!         <html>
34//!             <head/>
35//!             <body>
36//!                 <div>
37//!                     <p>"Counter: " { count }</p>
38//!                     <button on_click={decrement}>"-"</button>
39//!                     <button on_click={increment}>"+"</button>
40//!                 </div>
41//!             </body>
42//!         </html>
43//!     }
44//! }
45//! ```
46//!
47//! ## Example 2
48//!
49//! ```rust
50//! use vertigo::{css, component, DomNode, Value, dom, main};
51//!
52//! #[component]
53//! pub fn MyMessage(message: Value<String>) {
54//!     dom! {
55//!         <p>
56//!             "Message to the world: "
57//!             { message }
58//!         </p>
59//!     }
60//! }
61//!
62//! #[main]
63//! fn app() -> DomNode {
64//!     let message = Value::new("Hello world!".to_string());
65//!
66//!     let main_div = css!("
67//!         color: darkblue;
68//!     ");
69//!
70//!     dom! {
71//!         <html>
72//!             <head/>
73//!             <body>
74//!                 <div css={main_div}>
75//!                     <MyMessage message={message} />
76//!                 </div>
77//!             </body>
78//!         </html>
79//!     }
80//! }
81//! ```
82//!
83//! To get started you may consider looking at the
84//! [Tutorial](https://github.com/vertigo-web/vertigo/blob/master/tutorial.md).
85//!
86//! Short-links to most commonly used things:
87//!
88//! * [dom!] - Build [DomNode] using RSX/rstml (HTML-like) syntax
89//! * [css!] - Build [Css] using CSS-like syntax
90//! * [component] - Wrap function to be used as component in RSX
91//! * [main] - Wrap function to be vertigo entry-point
92//! * [get_driver] - Access browser facilities
93//! * [bind!] - Auto-clone variables before use
94//! * [Value] - Read-write reactive value
95//! * [Computed] - Read-only (computed) reactive value
96//! * [router::Router] - Hash or history routing
97
98#![deny(rust_2018_idioms)]
99#![feature(try_trait_v2)] // https://github.com/rust-lang/rust/issues/84277
100
101mod computed;
102mod css;
103mod dom;
104mod dom_macro;
105mod driver_module;
106mod fetch;
107mod future_box;
108pub mod inspect;
109mod instant;
110mod render;
111pub mod router;
112#[cfg(test)]
113mod tests;
114mod websocket;
115
116use computed::struct_mut::ValueMut;
117
118pub use computed::{
119    context::Context, struct_mut, AutoMap, Computed, Dependencies, DropResource, GraphId, Reactive,
120    ToComputed, Value,
121};
122
123pub use css::{
124    css_structs::{Css, CssGroup},
125    tailwind_class::TwClass,
126};
127
128pub use dom::{
129    attr_value::{AttrValue, CssAttrValue},
130    callback::{Callback, Callback1},
131    dom_comment::DomComment,
132    dom_element::DomElement,
133    dom_element_ref::DomElementRef,
134    dom_id::DomId,
135    dom_node::DomNode,
136    dom_text::DomText,
137    events::{ClickEvent, DropFileEvent, DropFileItem, KeyDownEvent},
138};
139pub use dom_macro::{AttrGroup, AttrGroupValue, EmbedDom};
140pub use driver_module::{
141    api::ApiImport,
142    dom_command::DriverDomCommand,
143    driver::{Driver, FetchMethod, FetchResult},
144    js_value::{
145        from_json, to_json, JsJson, JsJsonContext, JsJsonDeserialize, JsJsonObjectBuilder,
146        JsJsonSerialize, JsValue, MemoryBlock,
147    },
148};
149use driver_module::{api::CallbackId, init_env::init_env};
150pub use fetch::{
151    lazy_cache::{self, LazyCache},
152    request_builder::{RequestBody, RequestBuilder, RequestResponse},
153    resource::Resource,
154};
155pub use future_box::{FutureBox, FutureBoxSend};
156pub use instant::{Instant, InstantType};
157pub use websocket::{WebsocketConnection, WebsocketMessage};
158
159/// Allows to include a static file
160///
161/// This will place the file along with the rest of generated files. The macro returns a public path to the file with it's hash in name.
162pub use vertigo_macro::include_static;
163
164/// Allows to trace additional tailwind class names.
165///
166/// To use tailwind class name outside of literal tw attribute value, wrap it with `tw!` macro, so it gets traced by tailwind bundler.
167///
168/// ```rust
169/// use vertigo::{dom, tw};
170///
171/// let my_class = tw!("flex");
172///
173/// dom! {
174///     <div tw={my_class}>
175///         <p>"One"</p>
176///         <p>"Two"</p>
177///     </div>
178/// };
179/// ```
180pub use vertigo_macro::tw;
181
182/// Allows to conveniently clone values into closure.
183///
184/// ```rust
185/// use vertigo::{bind, dom, Value};
186///
187/// let count = Value::new(0);
188///
189/// let increment = bind!(count, |_| {
190///     count.change(|value| {
191///         *value += 1;
192///     });
193/// });
194///
195/// dom! {
196///     <div>
197///         <p>"Counter: " { count }</p>
198///         <button on_click={increment}>"+"</button>
199///     </div>
200/// };
201/// ```
202///
203/// Binding complex names results in last part being accessible inside:
204///
205/// ```rust
206/// use vertigo::bind;
207///
208/// struct Point {
209///     pub x: i32,
210///     pub y: i32,
211/// }
212///
213/// let point = Point { x: 1, y: 2 };
214///
215/// let callback = bind!(point.x, point.y, || {
216///     println!("Point: ({x}, {y})");
217/// });
218/// ```
219pub use vertigo_macro::bind;
220
221/// Allows to create an event handler based on provided arguments which is wrapped in Rc
222pub use vertigo_macro::bind_rc;
223
224/// Allows to create an event handler based on provided arguments which launches an asynchronous action
225pub use vertigo_macro::bind_spawn;
226
227/// Macro for creating `JsJson` from structures and structures from `JsJson`.
228///
229/// Used for fetching and sending objects over the network.
230///
231/// Enums representation is compatible with serde's "external tagging" which is the default.
232///
233/// ```rust
234/// #[derive(vertigo::AutoJsJson)]
235/// pub struct Post {
236///     pub id: i64,
237///     pub name: String,
238///     pub visible: bool,
239/// }
240///
241/// let post = Post {
242///     id: 1,
243///     name: "Hello".to_string(),
244///     visible: true
245/// };
246///
247/// let js_json = vertigo::to_json(post);
248///
249/// let post2 = vertigo::from_json::<Post>(js_json);
250/// ```
251pub use vertigo_macro::AutoJsJson;
252
253/// Macro which transforms a provided function into a component that can be used in [dom!] macro
254///
255/// ```rust
256/// use vertigo::prelude::*;
257///
258/// #[component]
259/// pub fn Header(name: Value<String>) {
260///     dom! {
261///         <div>"Hello" {name}</div>
262///     }
263/// }
264///
265/// let name = Value::new("world".to_string());
266///
267/// dom! {
268///     <div>
269///        <Header name={name} />
270///     </div>
271/// };
272/// ```
273///
274/// ```rust
275/// use vertigo::{bind, component, dom, AttrGroup, Value};
276///
277/// #[component]
278/// pub fn Input<'a>(label: &'a str, value: Value<String>, input: AttrGroup) {
279///     let on_input = bind!(value, |new_value: String| {
280///         value.set(new_value);
281///     });
282///
283///     dom! {
284///         <div>
285///             {label}
286///             <input {value} {on_input} {..input} />
287///         </div>
288///     }
289/// }
290///
291/// let value = Value::new("world".to_string());
292///
293/// dom! {
294///     <div>
295///        <Input label="Hello" {value} input:name="hello_value" />
296///     </div>
297/// };
298/// ```
299///
300/// Note: [AttrGroup] allows to dynamically pass arguments to some child node.
301pub use vertigo_macro::component;
302
303/// Macro that allows to evaluate pseudo-JavaScript expressions.
304///
305/// Example 1:
306///
307/// ```rust
308/// use vertigo::js;
309///
310/// let referrer = js!{ document.referrer };
311/// ```
312///
313/// Example 2:
314///
315/// ```rust
316/// # use vertigo::js;
317/// let max_y = js!{ window.scrollMaxY };
318/// js! { window.scrollTo(0, max_y) };
319/// ```
320///
321/// Can be used with [DomElementRef]:
322///
323/// ```rust
324/// use vertigo::{js, dom_element};
325///
326/// let node = dom_element! { <input /> };
327/// let node_ref = node.get_ref();
328/// js! { #node_ref.focus() };
329/// ```
330///
331/// Passing an object as an argument is a little more complicated, but possible:
332///
333/// ```rust
334/// # use vertigo::js;
335/// js! {
336///     window.scrollTo(
337///         vec![
338///             ("top", 100000.into()),
339///             ("behavior", "smooth".into()),
340///         ]
341///     )
342/// };
343/// ```
344#[macro_export]
345macro_rules! js {
346    // Convert `#ref_node.anything` into `#[ref_node] anything` which can be handled by js_inner macro.
347    ( #$ident:ident.$expr:expr ) => {
348        $crate::js_inner! { #[$ident] $expr }
349    };
350    // Otherwise be transparent.
351    ( $expr:expr ) => {
352        $crate::js_inner! { $expr }
353    };
354}
355
356/// Used internally by [js!] macro.
357pub use vertigo_macro::js_inner;
358
359/// Marco that marks an entry point of the app
360///
361/// Note: Html, head and body tags are required by vertigo to properly take over the DOM
362///
363/// Note 2: When using external tailwind, make sure the source `tailwind.css` file is in the same directory as usage of this macro.
364///
365/// ```rust
366/// use vertigo::prelude::*;
367///
368/// #[vertigo::main]
369/// fn app() -> DomNode {
370///     dom! {
371///         <html>
372///             <head/>
373///             <body>
374///                 <div>"Hello world"</div>
375///             </body>
376///         </html>
377///     }
378/// }
379/// ```
380pub use vertigo_macro::main;
381
382// Export log module which can be used in vertigo plugins
383pub use log;
384
385/// Allows to create [DomNode] using RSX/rstml (HTML-like) syntax.
386///
387/// Simple DOM with a param embedded:
388///
389/// ```rust
390/// use vertigo::dom;
391///
392/// let value = "world";
393///
394/// dom! {
395///     <div>
396///         <h3>"Hello " {value} "!"</h3>
397///         <p>"Good morning!"</p>
398///     </div>
399/// };
400/// ```
401///
402/// Mapping and embedding an `Option`:
403///
404/// ```rust
405/// use vertigo::dom;
406///
407/// let name = "John";
408/// let occupation = Some("Lumberjack");
409///
410/// dom! {
411///     <div>
412///         <h3>"Hello " {name} "!"</h3>
413///         {..occupation.map(|occupation| dom! { <p>"Occupation: " {occupation}</p> })}
414///     </div>
415/// };
416/// ```
417///
418/// Note the spread operator which utilizes the fact that `Option` is iterable in Rust.
419pub use vertigo_macro::dom;
420
421/// Allows to create [DomElement] using HTML tags.
422///
423/// Unlike [DomNode] generated by the [dom!] macro, it can't generate multiple nodes at top level,
424/// but allows to mangle with the outcome a little more, for example using [DomElement::add_child].
425pub use vertigo_macro::dom_element;
426
427/// Version of [dom!] macro that additionally emits compiler warning with generated code.
428pub use vertigo_macro::dom_debug;
429
430/// Allows to create [Css] styles for usage in [dom!] macro.
431///
432/// ```rust
433/// use vertigo::{css, dom};
434///
435/// let green_on_red = css!("
436///     color: green;
437///     background-color: red;
438/// ");
439///
440/// dom! {
441///    <div css={green_on_red}>"Tomato stem"</div>
442/// };
443/// ```
444///
445/// ```rust
446/// use vertigo::{css, Css, dom};
447///
448/// fn css_menu_item(active: bool) -> Css {
449///     let bg_color = if active { "lightblue" } else { "lightgreen" };
450///
451///     css! {"
452///         cursor: pointer;
453///         background-color: {bg_color};
454///
455///         :hover {
456///             text-decoration: underline;
457///         }
458///     "}
459/// }
460///
461/// dom! {
462///     <a css={css_menu_item(true)}>"Active item"</a>
463///     <a css={css_menu_item(false)}>"Inactive item"</a>
464/// };
465/// ```
466///
467/// See [tooltip demo](https://github.com/vertigo-web/vertigo/blob/master/demo/app/src/app/styling/tooltip.rs) for more complex example.
468pub use vertigo_macro::css;
469
470/// Constructs a CSS block that can be manually pushed into existing [Css] styles instance.
471///
472/// ```rust
473/// use vertigo::{css, css_block};
474///
475/// let mut green_style = css!("
476///     color: green;
477/// ");
478///
479/// green_style.push_str(
480///     css_block! { "
481///         font-style: italic;
482///    " }
483/// );
484/// ```
485pub use vertigo_macro::css_block;
486
487pub mod html_entities;
488
489pub struct DriverConstruct {
490    driver: Driver,
491    subscription: ValueMut<Option<DomNode>>,
492}
493
494impl DriverConstruct {
495    fn new() -> DriverConstruct {
496        let driver = Driver::default();
497
498        DriverConstruct {
499            driver,
500            subscription: ValueMut::new(None),
501        }
502    }
503
504    fn set_root(&self, root_view: DomNode) {
505        self.subscription.set(Some(root_view));
506    }
507}
508
509thread_local! {
510    static DRIVER_BROWSER: DriverConstruct = DriverConstruct::new();
511}
512
513fn start_app_inner(root_view: DomNode) {
514    get_driver_state("start_app", |state| {
515        init_env(state.driver.inner.api.clone());
516        state.driver.inner.api.on_fetch_start.trigger(());
517
518        state.set_root(root_view);
519
520        state.driver.inner.api.on_fetch_stop.trigger(());
521        get_driver().inner.dom.flush_dom_changes();
522    });
523}
524
525/// Starting point of the app (used by [main] macro, which is preferred)
526pub fn start_app(init_app: fn() -> DomNode) {
527    get_driver_state("start_app", |state| {
528        init_env(state.driver.inner.api.clone());
529
530        let dom = init_app();
531        start_app_inner(dom);
532    });
533}
534
535/// Getter for [Driver] singleton.
536///
537/// ```rust
538/// use vertigo::get_driver;
539///
540/// let number = get_driver().get_random(1, 10);
541/// ```
542pub fn get_driver() -> Driver {
543    DRIVER_BROWSER.with(|state| state.driver)
544}
545
546/// Do bunch of operations on dependency graph without triggering anything in between.
547pub fn transaction<R, F: FnOnce(&Context) -> R>(f: F) -> R {
548    get_driver().transaction(f)
549}
550
551pub mod prelude {
552    pub use crate::{bind, component, css, dom, Computed, Css, DomNode, ToComputed, Value};
553}
554
555//------------------------------------------------------------------------------------------------------------------
556// Internals below
557//------------------------------------------------------------------------------------------------------------------
558
559pub use driver_module::driver::{
560    VERTIGO_MOUNT_POINT_PLACEHOLDER, VERTIGO_PUBLIC_BUILD_PATH_PLACEHOLDER,
561};
562
563fn get_driver_state<R: Default, F: FnOnce(&DriverConstruct) -> R>(
564    label: &'static str,
565    once: F,
566) -> R {
567    match DRIVER_BROWSER.try_with(once) {
568        Ok(value) => value,
569        Err(_) => {
570            if label != "free" {
571                println!("error access {label}");
572            }
573
574            R::default()
575        }
576    }
577}
578
579// Methods for memory allocation
580
581#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
582#[no_mangle]
583pub fn alloc(size: u32) -> u32 {
584    get_driver_state("alloc", |state| {
585        state.driver.inner.api.arguments.alloc(size)
586    })
587}
588
589#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
590#[no_mangle]
591pub fn free(pointer: u32) {
592    get_driver_state("free", |state| {
593        state.driver.inner.api.arguments.free(pointer);
594    })
595}
596
597// Callbacks gateways
598
599#[no_mangle]
600pub fn wasm_callback(callback_id: u64, value_ptr: u32) -> u64 {
601    get_driver_state("export_dom_callback", |state| {
602        let value = state.driver.inner.api.arguments.get_by_ptr(value_ptr);
603        let callback_id = CallbackId::from_u64(callback_id);
604
605        let mut result = JsValue::Undefined;
606
607        state.driver.transaction(|_| {
608            result = state
609                .driver
610                .inner
611                .api
612                .callback_store
613                .call(callback_id, value);
614        });
615
616        if result == JsValue::Undefined {
617            return 0;
618        }
619
620        let memory_block = result.to_snapshot();
621        let (ptr, size) = memory_block.get_ptr_and_size();
622        state.driver.inner.api.arguments.set(memory_block);
623
624        let ptr = ptr as u64;
625        let size = size as u64;
626
627        (ptr << 32) + size
628    })
629}