1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
//! Tools for declaratively constructing and incrementally updating HTML DOM
//! trees on the web. Based on the [`moxie`] UI runtime.

#![deny(clippy::all, missing_docs)]

/// Internal macros for stamping out types to match stringly-typed web APIs.
#[macro_use]
mod macros;

pub(crate) mod cached_node;
pub mod elements;
pub mod embed;
pub mod interfaces;
pub mod text;

/// A module for glob-importing the most commonly used moxie-dom items.
pub mod prelude {
    #[cfg(feature = "webdom")]
    pub use crate::raw::sys;
    pub use crate::raw::{document, event, Dom as _, Node};
    pub use moxie::{cache, cache_state, cache_with, once, once_with, state, Key};

    pub use crate::{
        elements::html,
        interfaces::{
            content_categories::{
                EmbeddedContent as _, FlowContent as _, FormAssociatedContent as _,
                HeadingContent as _, InteractiveContent as _, LabelableContent as _,
                ListedContent as _, MetadataContent as _, PhrasingContent as _,
                ResettableContent as _, SectioningContent as _, SubmittableContent as _,
            },
            element::Element as _,
            event_target::EventTarget as _,
            global_events::{GlobalEvent as _, GlobalEventHandler as _},
            html_element::HtmlElement as _,
            node::{Node as _, Parent as _},
        },
        text::text,
        Stateful,
    };
}

use std::fmt::Debug;

/// A stateful element within the application.
// TODO make a way for the invoker to pass extra args instead of default?
pub trait Stateful: Debug + Sized + 'static {
    /// The value returned from `update` on each revision.
    type Output: interfaces::node::Child;

    /// The type used to generate updates to the app, typically a wrapper around
    /// [`moxie::Key`]s.
    type Updater: From<moxie::Key<Self>>;

    /// Compute a new version of the output.
    fn tick(&self, updater: Self::Updater) -> Self::Output;
}

/// A "root" stateful element which can be booted directly without any
/// arguments.
pub trait Boot: Stateful + Default {
    /// Start the app running with the provided `root`.
    fn boot(root: impl Into<prelude::Node>) {
        boot(root, || {
            let (app, updater) = prelude::state(Self::default);
            app.tick(updater.into())
        });
    }
}

/// Produce an interactive entrypoint for the specified app type. Creates a
/// `#[wasm_bindgen]` export with the name of the app type prefixed with `boot`.
/// For example, `app_boot!(Example)` would export a JavaScript function named
/// `bootExample`.
#[macro_export]
macro_rules! app_boot {
    ($app:ty) => {
        moxie_dom::__paste! {
            impl moxie_dom::Boot for $app {}

            #[moxie_dom::__wasm_bindgen(js_name = [<boot $app>])]
            #[doc(hidden)]
            pub fn [<__js_boot_ $app:snake>] (root: moxie_dom::raw::sys::Node) {
                <$app as moxie_dom::Boot>::boot(root);
            }
        }
    };
}

#[cfg(feature = "webdom")]
#[doc(hidden)]
pub use wasm_bindgen::prelude::wasm_bindgen as __wasm_bindgen;

#[doc(hidden)]
pub use paste::paste as __paste;

/// Provides the underlying DOM implementation for moxie-dom.
pub use augdom as raw;

/// The "boot sequence" for a moxie-dom instance creates a
/// [crate::embed::WebRuntime] with the provided arguments and begins scheduling
/// its execution with `requestAnimationFrame` on state changes.
///
/// If you need to schedule your root function more or less frequently than when
/// state variables are updated, see the [embed](crate::embed) module for
/// granular control over scheduling.
///
/// In terms of the embed module's APIs, this function constructs a new
/// [`WebRuntime`](crate::embed::WebRuntime) and begins scheduling it with an
/// [`AnimationFrameScheduler`](raf::AnimationFrameScheduler) which requests an
/// animation frame only when there are updates to state variables.
///
/// Requires the `webdom` feature.
#[cfg(any(feature = "webdom", doc))]
pub fn boot<Root>(new_parent: impl Into<augdom::Node>, root: impl FnMut() -> Root + 'static)
where
    Root: interfaces::node::Child + 'static,
{
    embed::WebRuntime::new(new_parent.into(), root).animation_frame_scheduler().run_on_wake();
}

/// Runs the provided closure once and produces a prettified HTML string from
/// the contents.
///
/// If you need more control over the output of the HTML, see the implementation
/// of this function.
///
/// Requires the `rsdom` feature.
#[cfg(any(feature = "rsdom", doc))]
pub fn render_html<Root>(root: impl FnMut() -> Root + 'static) -> String
where
    Root: interfaces::node::Child + 'static,
{
    use augdom::Dom;

    let (mut tester, root) = embed::WebRuntime::in_rsdom_div(root);
    tester.run_once();
    let outer = root.pretty_outer_html(2);
    // TODO(#185) remove this hack
    // because we use the indented version, we know that only at the top and bottom
    // is what we want
    outer.lines().filter(|l| *l != "<div>" && *l != "</div>").map(|l| l.split_at(2).1).fold(
        String::new(),
        |mut fragment, line| {
            if !fragment.is_empty() {
                fragment.push('\n');
            }
            fragment.push_str(line);
            fragment
        },
    )
}

#[cfg(test)]
mod tests {
    use crate::{
        boot,
        elements::html::{b, div, p},
        prelude::*,
    };
    use pretty_assertions::assert_eq;
    use wasm_bindgen_test::*;

    wasm_bindgen_test_configure!(run_in_browser);

    #[wasm_bindgen_test]
    pub async fn hello_browser() {
        let root = augdom::document().create_element("div");
        boot(root.clone(), || {
            mox::mox! {
                <div>
                    <p>"hello browser"</p>
                    <div>
                        <p><b>"looooool"</b></p>
                    </div>
                </div>
            }
        });

        assert_eq!(
            root.pretty_outer_html(2),
            r#"<div>
  <div>
    <p>hello browser</p>
    <div>
      <p>
        <b>looooool</b>
      </p>
    </div>
  </div>
</div>"#
        );
    }
}